- dbt プロジェクトを作成し、ClickHouse アダプターをセットアップする。
- モデルを定義する。
- モデルを更新する。
- incremental モデルを作成する。
- snapshot モデルを作成する。
- materialized view を使用する。
セットアップ
ClickHouse を準備する
テーブル
roles の created_at カラムには、デフォルト値として now() が設定されています。これは後でモデルのインクリメンタル更新を識別するために使用します。詳しくはインクリメンタルモデルを参照してください。s3 関数を使用して、公開エンドポイントからソースデータを読み込み、データを挿入します。次のコマンドを実行してテーブルにデータを投入してください。
ClickHouse への接続
-
dbt プロジェクトを作成します。この例では、
imdbソースにちなんで名前を付けます。プロンプトが表示されたら、データベースとしてclickhouseを選択します。 -
プロジェクトフォルダーに
cdします。 - この時点で、お好みのテキストエディターが必要です。以下の例では、広く使われている VS Code を使用します。IMDB ディレクトリを開くと、yml ファイルと sql ファイルが一式表示されるはずです。
-
dbt_project.ymlファイルを更新し、最初のモデルactor_summaryを指定して、プロファイルをclickhouse_imdbに設定します。 -
次に、dbt に ClickHouse インスタンスの接続情報を指定する必要があります。以下を
~/.dbt/profiles.ymlに追加してください。user と password は変更が必要である点に注意してください。利用可能な追加の設定はこちらに記載されています。 -
IMDB ディレクトリから
dbt debugコマンドを実行し、dbt が ClickHouse に接続できるかどうかを確認します。接続に成功したことを示すConnection test: [OK connection ok]が応答に含まれていることを確認してください。
シンプルなビュー マテリアライゼーションの作成
CREATE VIEW AS ステートメントによってビューとして再構築されます。これにはデータを追加で保存する必要はありませんが、テーブル マテリアライゼーションよりクエリは遅くなります。
-
imdbフォルダ内のmodels/exampleディレクトリを削除します: -
modelsフォルダ内のactorsに新しいファイルを作成します。ここでは、それぞれが actor モデルを表すファイルを作成します。 -
models/actorsフォルダにschema.ymlとactor_summary.sqlを作成します。ファイルschema.ymlでテーブルを定義します。これらは以降、マクロで利用できるようになります。models/actors/schema.ymlを次の内容に編集してください:actors_summary.sqlでは、実際のモデルを定義します。config 関数では、このモデルを ClickHouse で view として materialize するよう指定している点にも注意してください。各テーブルは、schema.ymlファイルから関数sourceを介して参照されます。たとえばsource('imdb', 'movies')は、imdbデータベース内のmoviesテーブルを参照します。models/actors/actors_summary.sqlを編集して、次の内容にしてください:最終的な actor_summary にカラムupdated_atを含めている点に注目してください。これは後で増分マテリアライゼーションに利用します。 -
imdbディレクトリでdbt runコマンドを実行します。 -
dbt は、指定どおりにこのモデルを ClickHouse のビューとして表現します。これで、このビューに直接クエリを実行できます。このビューは
imdb_dbtデータベースに作成されます。これは、clickhouse_imdbプロファイル配下の~/.dbt/profiles.ymlファイルにあるスキーマ パラメータによって決まります。このビューに対してクエリを実行すると、よりシンプルな構文で先ほどのクエリと同じ結果を再現できます:
テーブルとしてのマテリアライゼーションの作成
INSERT TO SELECT が実行されます。このテーブルは毎回再構築されるため、増分ではない点に注意してください。そのため、結果セットが大きい場合は実行時間が長くなる可能性があります。詳細は dbt Limitations を参照してください。
-
actors_summary.sqlファイルを修正し、materializedパラメータがtableに設定されるようにします。ORDER BYがどのように定義されているか、またMergeTreeテーブルエンジンを使用していることに注目してください。 -
imdbディレクトリでdbt runコマンドを実行します。この処理にはやや時間がかかる場合があり、ほとんどのマシンでは約 10 秒です。 -
imdb_dbt.actor_summaryテーブルが作成されたことを確認します。適切なデータ型を持つテーブルが表示されるはずです。 -
このテーブルの結果が前のレスポンスと一致していることを確認します。モデルがテーブルになったことで、レスポンス時間が大幅に改善されていることがわかります。
このモデルに対して、他のクエリも自由に実行してみてください。たとえば、出演回数が 5 回を超える俳優のうち、平均評価が最も高いのは誰でしょうか。
インクリメンタルマテリアライゼーションの作成
inserts_only モードをサポートしています。このモードでは、一時テーブルを作成せずに、すべての更新がターゲットテーブルに挿入されます (詳細は後述します) 。
この例を示すために、俳優「Clicky McClickHouse」を追加します。彼は驚異の 910 本の映画に出演し、Mel Blanc をも上回る出演本数になります。
-
まず、モデルを
incremental型に変更します。この変更には、次の対応が必要です。- unique_key - アダプターが行を一意に識別できるようにするには、unique_key を指定する必要があります。この場合は、クエリ内の
idフィールドで十分です。これにより、マテリアライズされたテーブル内で行の重複が発生しないようにできます。一意性制約の詳細については、こちらを参照してください。 - Incremental filter - また、増分実行時にどの行が変更されたかを dbt がどのように識別するかも指定する必要があります。これは、差分を表す式を指定することで実現します。通常、イベントデータでは timestamp を使うため、ここでは
updated_attimestamp フィールドを使用します。このカラムは、行が挿入されると既定で now() の値が設定されるため、新しい行を特定できます。さらに、新しいアクターが追加されるケースも検出する必要があります。既存のマテリアライズされたテーブルを表す{{this}}変数を使うと、式はwhere id > (select max(id) from {{ this }}) or updated_at > (select max(updated_at) from {{this}})になります。これを{% if is_incremental() %}条件内に埋め込むことで、増分実行時にのみ使用され、テーブルの初回作成時には使用されないようにしています。増分モデルで行をフィルタリングする詳細については、dbt ドキュメントのこちらの説明を参照してください。
actor_summary.sqlファイルを以下のように更新します。このモデルは、rolesテーブルとactorsテーブルに対する更新と追加にのみ応答する点に注意してください。すべてのテーブルに対応させるには、このモデルを複数のサブモデルに分割し、それぞれに独自の増分条件を設定することをおすすめします。こうしたモデルは、さらに相互に参照したり接続したりできます。モデルの相互参照の詳細については、こちらを参照してください。 - unique_key - アダプターが行を一意に識別できるようにするには、unique_key を指定する必要があります。この場合は、クエリ内の
-
dbt runを実行し、生成されたテーブルの結果を確認します: -
ここで、増分更新を示すために、モデルにデータを追加します。
actorsテーブルに、俳優 “Clicky McClickHouse” を追加してください: -
“Clicky”をランダムに選んだ910本の映画に出演させましょう:
-
基になるソーステーブルを直接クエリし、dbtモデルを介さずに、彼が現在、本当に最も出演回数の多い俳優であることを確認します:
-
dbt runを実行し、モデルが更新されており、上記の結果と一致していることを確認します:
内部
- アダプターは一時テーブル
actor_sumary__dbt_tmpを作成します。変更された行はこのテーブルに書き込まれます。 - 次に、新しいテーブル
actor_summary_new,が作成されます。続いて、古いテーブルの行が古いテーブルから新しいテーブルへ書き込まれますが、その際、一時テーブルに行 ID が存在しないことを確認します。これにより、更新と重複を効果的に処理できます。 - 一時テーブルの結果が、新しい
actor_summaryテーブルに書き込まれます。 - 最後に、新しいテーブルは
EXCHANGE TABLESステートメントを介して古いバージョンとアトミックに入れ替えられます。その後、古いテーブルと一時テーブルは削除されます。
Append 戦略 (insert のみモード)
incremental_strategy を使用します。これには append を設定できます。この値を設定すると、更新された行はターゲットテーブル (つまり imdb_dbt.actor_summary) に直接 insert され、一時テーブルは作成されません。
注: append only モードを使うには、データが不変であるか、重複を許容できる必要があります。変更された行に対応する増分テーブルモデルが必要な場合は、このモードは使用しないでください。
このモードを説明するために、新しい俳優をもう 1 人追加し、incremental_strategy='append' を指定して dbt run を再実行します。
-
actor_summary.sql で append only モードを設定します:
-
もう 1 人の有名な俳優、Danny DeBito を追加します
-
Danny をランダムに選んだ 920 本の映画に出演させます。
-
dbt run を実行し、Danny が actor_summary テーブルに追加されたことを確認します
imdb_dbt.actor_summary テーブルに追加され、テーブルは作成されません。
削除と挿入モード (実験的)
incremental_strategy パラメータでモデルに設定できます。
- アダプターが一時テーブル
actor_sumary__dbt_tmpを作成します。変更された行はこのテーブルに書き込まれます。 - 現在の
actor_summaryテーブルに対してDELETEを実行します。actor_sumary__dbt_tmpにある id をもとに行が削除されます。 actor_sumary__dbt_tmpの行を、INSERT INTO actor_summary SELECT * FROM actor_sumary__dbt_tmpを使ってactor_summaryに挿入します。
insert_overwrite モード (実験的)
- incremental モデルのリレーションと同じ構造を持つステージング (一時) テーブルを作成します:
CREATE TABLE {staging} AS {target}。 - 新しいレコード (SELECT によって生成されたもの) のみをステージングテーブルに挿入します。
- 新しいパーティション (ステージングテーブルに存在するもの) のみをターゲットテーブル内で置き換えます。
このアプローチには、次の利点があります。
- テーブル全体をコピーしないため、デフォルトの戦略より高速です。
- INSERT 操作が正常に完了するまで元のテーブルを変更しないため、他の戦略より安全です。途中で障害が発生した場合でも、元のテーブルは変更されません。
- データエンジニアリングにおける「パーティション不変性」というベストプラクティスに沿っています。これにより、インクリメンタル処理や並列データ処理、ロールバックなどが簡単になります。
スナップショットの作成
actor_summary.sql で inserts_only=True を設定していないことを確認してください。models/actor_summary.sql は次のようになっているはずです:
-
snapshots ディレクトリに
actor_summaryファイルを作成します。 -
actor_summary.sql ファイルの内容を、以下のように更新します。
selectクエリは、時間の経過に沿ってスナップショットとして保持したい結果を定義します。ref関数は、先ほど作成した actor_summary モデルを参照するために使用します。- レコードの変更を示すために、timestamp カラムが必要です。ここでは updated_at カラム (インクリメンタルテーブルモデルの作成 を参照) を使用できます。
strategyパラメータは、更新の判定に timestamp を使用することを示し、updated_atパラメータは使用するカラムを指定します。これがモデルに存在しない場合は、代わりに check strategy を使用することもできます。これはかなり非効率で、比較対象とするカラムの一覧をユーザーが指定する必要があります。dbt はこれらのカラムの現在値と履歴値を比較し、変更があれば記録します (同一であれば何もしません) 。
-
dbt snapshotコマンドを実行します。
actor_summary_snapshot テーブルが snapshots DB に作成されていることに注目してください (これは target_schema パラメータによって決まります) 。
-
このデータをサンプリングすると、dbt によって dbt_valid_from と dbt_valid_to というカラムが追加されていることがわかります。後者の値は null になっています。以降の実行でこれが更新されます。
-
お気に入りの俳優、Clicky McClickHouseをさらに10本の映画に出演させましょう。
-
imdbディレクトリで dbt run コマンドを再実行します。これにより、インクリメンタルモデルが更新されます。完了したら、変更を記録するために dbt snapshot を実行します。 -
ここでスナップショットをクエリすると、Clicky McClickHouse に対応する行が 2 行あることがわかります。前のエントリには dbt_valid_to の値が入るようになりました。新しい値は、dbt_valid_from カラムに同じ値で記録され、dbt_valid_to の値は null になっています。新しい行があった場合は、それらもスナップショットに追加されます。
seed の使用
-
既存のデータセットからジャンルコードの一覧を生成します。dbt ディレクトリで、
clickhouse-clientを使用してseeds/genre_codes.csvファイルを作成します。 -
dbt seedコマンドを実行します。これにより、CSV ファイルの行を使って、databaseimdb_dbtに新しい tablegenre_codesが作成されます (スキーマ設定で定義したとおりです) 。 -
これらが読み込まれたことを確認します。