🪴

コードリーディングで理解する dbt seed の仕組み

に公開

はじめに

こんにちは!ナウキャストのデータエンジニアのけびんです。

皆さん dbt の seed は使っていますか?ナウキャストでも使っている機能なのですが、利用する中で dbt seed の仕組みについて理解できていなかったポイントがあり、バッチジョブがエラーになってしまったことがありました。
本ブログでは dbt-adapter のコードも紹介しつつ dbt seed の仕組みについて解説します。

dbt seed とは?

dbt の Seed は CSV ファイルを dbt project で管理し、DWHにロードする機能です。変更頻度はそこまで高くないが、バージョン管理したりレビュー体制を用意しておきたいような小さめのデータの管理に適した機能です。
具体的には小さいディメンションテーブルであったり、絞り込み条件などに利用したいカラムの値のリストの管理などによく使われるかなと思います。

以下のドキュメントのサンプルを引用して仕組みをおさらいしておきましょう。
https://docs.getdbt.com/docs/build/seeds

まず以下のように CSV ファイルを dbt project 内で用意しておきます。

seeds/seed_country_codes.csv
country_code,country_name
US,United States
CA,Canada
GB,United Kingdom
...

あとは dbt seed コマンドを実行するだけで、 seed_country_codes という名前のテーブルが出来上がるというわけです。

$ dbt seed

Found 2 models, 3 tests, 0 archives, 0 analyses, 53 macros, 0 operations, 1 seed file

14:46:15 | Concurrency: 1 threads (target='dev')
14:46:15 |
14:46:15 | 1 of 1 START seed file analytics.seed_country_codes........................... [RUN]
14:46:15 | 1 of 1 OK loaded seed file analytics.seed_country_codes....................... [INSERT 3 in 0.01s]
14:46:16 |
14:46:16 | Finished running 1 seed in 0.14s.

Completed successfully

Done. PASS=1 ERROR=0 SKIP=0 TOTAL=1

普通のモデルのように ref で参照することができます。

models/orders.sql
select * from {{ ref('seed_country_codes') }}

seed に関しては prefix として seed_ をつけるようにしておくとぱっと見で seed を使っていることがわかるのでおすすめです。

また dbt build コマンドには seed も含まれるので、 build を使えば依存関係を踏まえ適切に seed の実行も可能です。

dbt seed で実行されるクエリ

dbt seed のコマンドを実行した際に、具体的にどのようなクエリが発行されるかご存知でしょうか?

普通の dbt model のように、毎回 CREATE OR REPLACE TABLE の CTAS クエリを実行しているのかなと自分は思っていたのですが、実は違います。実際には実行タイミング・オプションによって異なるのですが、

  • 初回実行時・--full-refresh 指定時
    • (table が存在すれば) DROP TABLE IF EXISTS {{ relation }} CASCADE
    • CREATE TABLE
    • INSERT
  • 2回目以降の実行時
    • TRUNCATE
    • INSERT

という挙動になります。

この挙動のため、seedに関して「カラムの追加」や「カラム名の変更」などがあったりすると dbt seed の実行時にエラーになります。このことは seed の FAQ にある "The columns of my seed changed, and now I get an error when running the seed command, what should I do?" でも触れられています。
https://docs.getdbt.com/docs/build/seeds#faqs

この内容を引用すると、

$ dbt seed
Running with dbt=1.6.0-rc2
Found 0 models, 0 tests, 0 snapshots, 0 analyses, 130 macros, 0 operations, 1 seed file, 0 sources

12:12:27 | Concurrency: 8 threads (target='dev_snowflake')
12:12:27 |
12:12:27 | 1 of 1 START seed file dbt_claire.seed_country_codes...................... [RUN]
12:12:30 | 1 of 1 ERROR loading seed file dbt_claire.seed_country_codes.............. [ERROR in 2.78s]
12:12:31 |
12:12:31 | Finished running 1 seed in 10.05s.

Completed with 1 error and 0 warnings:

Database Error in seed country_codes (seeds/seed_country_codes.csv)
  000904 (42000): SQL compilation error: error line 1 at position 62
  invalid identifier 'COUNTRY_NAME'

Done. PASS=0 WARN=0 ERROR=1 SKIP=0 TOTAL=1

このようなスキーマの変更で drop cascade を実行すると下流のモデルに影響がでる可能性があるため、エラーになるようにすることで安全な実装にしているようです。
スキーマが変更される際には下流のモデルへの影響に注意して dbt seed --full-refresh でフルリフレッシュしましょう。

このように dbt は便利ですが、その裏では様々なクエリが実行されています。dbtが 行なっていることを推測するのではなく、実際のクエリに向き合うことが非常に重要です。

dbt seed の YAML ファイルの書き方について

seed に関する小ネタです。
dbt seed でもメタデータ管理のために普通の dbt model と同様 YAML ファイルを書くと思いますが、実はカラムのデータ型の書き方が普通の dbt model とは少し異なります。通常のモデルだと columns の中に諸々書くと思いますが、以下の通り config → column_types の中に書く必要があるので注意しましょう。

seeds/properties.yml
version: 2

seeds:
  - name: country_codes
    config:
      column_types:
        country_code: varchar(2)
        country_name: varchar(32)

https://docs.getdbt.com/reference/seed-properties
https://docs.getdbt.com/reference/resource-configs/column_types

dbt-adapters のコードリーディング

上記の挙動の確認は Snowflake であれば Query History を眺めておくのが手っ取り早いです。しかし細かい部分を把握したければ dbt-adapters のコードを読むのが良いです。

seed のクエリ

seed の実際のマクロは以下のあたりになります。
テーブルが存在していたら reset_csv_table というマクロを呼び出し、そうでなければ create_csv_table を呼ぶと分岐していることが確認できます。
https://github.com/dbt-labs/dbt-adapters/blob/c60673bc11a238fceb5b8764840d1ee675d091bf/dbt-adapters/src/dbt/include/global_project/macros/materializations/seeds/seed.sql#L22-L38

実際の reset_csv_table のマクロはこちらに記載があります。 --full-refresh モードであれば一度 drop relation してから create_csv_table のマクロを実行する一方、そうでなければ truncate しておく、という分岐の実装が確認できます。
https://github.com/dbt-labs/dbt-adapters/blob/c60673bc11a238fceb5b8764840d1ee675d091bf/dbt-adapters/src/dbt/include/global_project/macros/materializations/seeds/helpers.sql#L29-L44

insert のクエリは load_csv_rows のマクロで作られていますがこちらについては dbt-snowflake の中で詳細を確認することができます。
https://github.com/dbt-labs/dbt-adapters/blob/c60673bc11a238fceb5b8764840d1ee675d091bf/dbt-snowflake/src/dbt/include/snowflake/macros/materializations/seed.sql#L15-L28

このように、 dbt-adapters の多くのマクロは jinja テンプレートで書かれているだけなので、意外と読みやすいです。
詳細な実装を把握するのにはコードリーディングが一番なのでぜひ呼んでみてください。

seed の config

以下のコードを読むと config → column_types を元に CREATE TABLE のクエリが構成されていることがわかります。
https://github.com/dbt-labs/dbt-adapters/blob/1f31e6a8649e0528256d81a7dfde67a31da2b07b/dbt-adapters/src/dbt/include/global_project/macros/materializations/seeds/helpers.sql#L6-L26

まとめ

dbt seed に関する仕組みついて紹介しました。
パフォーマンス改善の文脈で「推測するな、計測せよ」と言われたりしますが、dbtを利用する際にも dbt が実行していることを推測するのではなく、実際に実行されるクエリと向き合うことが大事になります。また dbt-adapter のコードを読んだりしてみると多くの学びがありました。

皆さんも dbt seed に関する知見があれば教えてください!

GitHubで編集を提案
Finatext Tech Blog

Discussion