前回に引き続き、dbtの運用メモです。簡単にまとめると、こういう感じです。
- dbtを使ってDWHを構築しているが、バージョン管理している
- dbtの都合上、バージョン間の差分が確認しにくい
- いい感じに差分を確認できるスクリプトを作った
背景
背景: dbtの制約でnameを一意にしないといけない
dbtのpropertiesはymlファイルに書けて、モデルのnameはここに書かれる。例えばこういう感じ。
version: 2 models: - name: my_first_dbt_model description: "A starter dbt model" columns: - name: id description: "The primary key for this table" tests: - unique - not_null - name: my_second_dbt_model description: "A starter dbt model" columns: - name: id description: "The primary key for this table" tests: - unique - not_null
ここに書かれるnameはファイル名と紐付けられ、かつ一意にする必要がある。
Do model names need to be unique? Yes! To build dependencies between models, you need to use the ref function. The ref function only takes one argument — the model name (i.e. the filename). As a result, these model names need to be unique, even if they are in distinct folders.
グローバルに一意になるように人間が気を付けるのは本当に面倒なので(マジでどうにかなって欲しい...)、ある程度機械的にnameを付けたファイルを生成してくれるdbt-helperを利用しています。これを利用してファイルを作っていくと、こういう形になります。
models/my_datawarehouse_dev/user/intermediate/my_datawarehouse_dev__user__intermediate__user_some_attribute.sql
このファイル名のまま素朴にdbt run
すると、めちゃくちゃ長い名前のテーブルが生成されて使いにくいです。実際はカスタムエイリアスを併用することになるでしょう。
背景: DWHのバージョン管理をしている
しています。背景は昔に書いたので、そちらを読んでください。
課題: バージョン間の差分を知るのが難しい
これまで述べたように、dbtを使いつつ、DWHのバージョン管理をしようとすると、ディレクトリ構成がこういう感じになってきます(intermediate配下には他にもdbtで分割した10個くらいSQLファイルがあったりする)。
models/my_datawarehouse_dev/user_versionized/user_v0_0_1/intermediate/my_datawarehouse_dev__user_versionized__user_v0_0_1__intermediate__user_some_attribute.sql
models/my_datawarehouse_dev/user_versionized/user_v0_0_2/intermediate/my_datawarehouse_dev__user_versionized__user_v0_0_2__intermediate__user_some_attribute.sql
models/my_datawarehouse_dev/user_versionized/user_v0_0_3/intermediate/my_datawarehouse_dev__user_versionized__user_v0_0_3__intermediate__user_some_attribute.sql
(心の声: ファイル名が長すぎて横にブチ抜けてしまった...!)
データの検証をするときやユーザーからの問い合わせで「v0.0.1とv.0.0.2の違いをSQLレベルでさっと知りたい」という場合があります。ディレクトリ間の差分についてはdiff -r dir_a dir_b
のような形で出すこともできますが、ファイル名が同じ場合のみです。今回の場合はディレクトリ名やファイル名も異なるので、素朴にはバージョン間の差分を出すことができません。困った。
解決法: 差分を出すスクリプトを自前で書く
何とかできないものかなと思ったけど、まあ自分でスクリプトを書きました。実際の中身は20行くらいなので、まあメンテナンスもそんなに大変ではないでしょう。中身でやっていることは
- compileしたSQLファイルで比較
- jinjaのテンプレートの変数などが展開された後の比較をしたいので
- バージョン間だけでなく、(GCPの)プロジェクト名が異なる場合にも対応できるように
- 開発環境は
my_datawarehouse_dev
プロジェクト、本番環境はmy_datawarehouse
プロジェクトでやっていたりするので
- 開発環境は
- 実際の差分を取るのは
diff -r
にお任せ - ファイルの適当のお掃除
という感じ。
#!/bin/bash set -eu -o pipefail # dbtのモデルファイルをバージョン跨ぎで比較するためのスクリプト # 基本的には`diff -r`で比較するが、そのためにファイル名やプロジェクト名をrenameする # sqlファイルを変更した場合にはcompileしなおす必要があるので注意(`dbt compile --target local --project-dir . --profiles-dir`) usage() { cat <<EOF usage: $0 --from_project FROM_PROJECT --from_dataset FROM_DATASET --from_table FROM_TABLE --to_project TO_PROJECT --to_dataset TO_DATASET --to_table TO_TABLE options: --from_project ベースとなるプロジェクト名 --from_dataset ベースとなるデータセット名 --from_table ベースとなるテーブル名 --to_project 比較対象のプロジェクト名 --to_dataset 比較対象のデータセット名 --to_table 比較対象のテーブル名 EOF exit 1 } # diffの比較対象A FROM_PROJECT="" FROM_DATASET="" FROM_TABLE="" # diffの比較対象B TO_PROJECT="" TO_DATASET="" TO_TABLE="" DBT_PROJECT_NAME="my_new_project" flag_parse() { for i in $*; do case $i in "--from_project") FROM_PROJECT="1" ;; "--from_dataset") FROM_DATASET="1" ;; "--from_table") FROM_TABLE="1" ;; "--to_project") TO_PROJECT="1" ;; "--to_dataset") TO_DATASET="1" ;; "--to_table") TO_TABLE="1" ;; "--dbt_project_name") DBT_PROJECT_NAME="1" ;; "--help") usage ;; *) [[ "${FROM_PROJECT}" == "1" ]] && FROM_PROJECT=$i && continue [[ "${FROM_DATASET}" == "1" ]] && FROM_DATASET=$i && continue [[ "${FROM_TABLE}" == "1" ]] && FROM_TABLE=$i && continue [[ "${TO_PROJECT}" == "1" ]] && TO_PROJECT=$i && continue [[ "${TO_DATASET}" == "1" ]] && TO_DATASET=$i && continue [[ "${TO_TABLE}" == "1" ]] && TO_TABLE=$i && continue [[ "${DBT_PROJECT_NAME}" == "1" ]] && DBT_PROJECT_NAME=$i && continue usage ;; esac shift done } # main [[ $# == "0" ]] && usage flag_parse $* tmpfile=$(mktemp -d dbt-diff-XXX) function rm_tmpfile { [[ -d "${tmpfile}" ]] && rm -rf "${tmpfile}" } trap rm_tmpfile EXIT # 正常終了したとき trap 'trap - EXIT; rm_tmpfile; exit -1' INT PIPE TERM # 異常終了したとき cp -r "../src/target/compiled/${DBT_PROJECT_NAME}/models/${FROM_PROJECT}/${FROM_DATASET}/${FROM_TABLE}" ${tmpfile}/a # uniquenessのテストなどに関するSQLも生成されるが、それは無視したいので削除 rm -rf ${tmpfile}/a/schema* cp -r "../src/target/compiled/${DBT_PROJECT_NAME}/models/${TO_PROJECT}/${TO_DATASET}/${TO_TABLE}" ${tmpfile}/b rm -rf ${tmpfile}/b/schema* # brew install rename rename -s ${TO_PROJECT} ${FROM_PROJECT} ${tmpfile}/b/{**,.}/*.sql for FILE in $(find "${tmpfile}/b" -maxdepth 3 -type f); do # brew install gnu-sed gsed -i s/${TO_PROJECT//_/-}/${FROM_PROJECT//_/-}/g ${FILE} done diff -rw ${tmpfile}/a ${tmpfile}/b
dbtは便利なんだけど、運用していると案外困り事が多いので、こういう知見をちまちまと書いていきたい。