dbtで素直にviewやtableで作っていた変換が、データが増えるにつれて重くなってきます。毎晩のfull-refreshが朝までに終わらない。日中に追いつかせようとするとウェアハウスが膨らんでコストが跳ねる。多くの現場で同じ壁にぶつかります。
そこで登場するのが、dbtのincremental modelsです。新しく増えたデータだけを処理してテーブルに追記する仕組みで、変換時間とコストを大きく下げられます。ただし設定を間違えると、重複・欠損・遅延の伝播といった問題が出ます。仕組みと設計判断を順に押さえます。
incrementalの仕組み
incremental modelsは、初回は全件を処理してテーブルを作り、2回目以降は「前回までに処理したデータ」と「今回新しく来たデータ」の差分だけを処理してテーブルに反映する、というモデルです。dbtは内部で「初回か増分か」を判定し、SQLを分岐させます。
| materialization | 毎回の動き | 向く場面 |
|---|---|---|
| view | クエリ定義のみ。実体なし | 軽い変換、データが少ない |
| table | 毎回テーブルを作り直す | 中規模、整合性が単純 |
| incremental | 初回は全件、以降は差分だけ追記 | 大規模、追加中心のデータ |
| snapshot | 履歴を保持するSCD Type 2用 | 変化する属性の履歴管理 |
dbtの基本的な層分けやmaterializationの考え方はdbtでの3層実装ガイドにまとめています。この記事では、その先のincremental設計について解説します。
最小のincrementalモデル
-- models/silver/fct_orders.sql
{{ config(
materialized='incremental',
unique_key='order_id'
) }}
SELECT
order_id,
customer_id,
order_total,
ordered_at
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
-- 前回より新しいレコードだけを取り込む
WHERE ordered_at > (SELECT MAX(ordered_at) FROM {{ this }})
{% endif %}
大事なのは2点です。`is_incremental()`で初回と差分実行を分岐させ、差分実行のときだけWHERE句で取り込み範囲を絞ること。そして`unique_key`を指定して、同じorder_idが来たら既存行を置き換えるよう指示することです。これで増分の重複を防ぎます。
incremental_strategy:append・merge・delete+insert
増分をどう取り込むかには、3つの戦略があります。挙動が違うので、ソースの特性に合わせて選びます。
| strategy | 動き | 向く場面 |
|---|---|---|
| append | 差分をそのまま追加。重複も書き込む | イベントログのように追加しかない |
| merge | unique_keyで一致したら更新、無ければ追加 | 更新がある(注文、顧客) |
| delete+insert | unique_keyで一致するものを削除してから挿入 | mergeが使えないDB、明示的に置き換えたい |
| insert_overwrite | 指定パーティションを丸ごと置き換える | BigQuery等で日付パーティション運用 |
SnowflakeやBigQueryではmergeが既定で、unique_keyがあれば普通はこれで足ります。BigQueryで日付パーティション運用なら、insert_overwriteが効率的です。クラウドDWHごとの違いはクラウドDWH入門を参照してください。
on_schema_change:カラム追加にどう備えるか
ソース側に新しいカラムが追加されたとき、incrementalモデルがどう振る舞うかを決めるのが`on_schema_change`です。デフォルトは`ignore`で、新しいカラムは無視されます。気づかず欠損する事故の温床なので、明示的に設定するのが安全です。
| 設定値 | 動き | 使いどころ |
|---|---|---|
| ignore(既定) | 新カラムを無視。気づかず欠損 | 推奨しない |
| append_new_columns | 新カラムを追加。過去行はNULL | 追加に強い運用 |
| sync_all_columns | 追加・削除・型変更を全部追従 | ソース変更が頻繁な場面 |
| fail | 変化を検知したらエラーで停止 | 厳密管理したい場面 |
{{ config(
materialized='incremental',
unique_key='order_id',
on_schema_change='append_new_columns'
) }}
よくある落とし穴
incremental設計でハマりやすい4つを挙げます。
- 遅延データで欠損する:ソースが「数日遅れて到着するレコード」を持つのに、`WHERE ts > MAX(ts)`で切ると、遅れて来たデータが永久に欠けます。「過去N日まで遡って再評価」の窓を持たせるのが定石です(例:`ts > DATEADD(day, -3, MAX(ts))`)。
- unique_key忘れで重複:append以外でunique_keyを指定しないと、同じレコードが二重登録されます。更新があるテーブルでは必須です。
- 大規模mergeが重い:mergeのターゲット側が巨大だと、毎回のmergeコストが膨らみます。日付パーティションを切って`insert_overwrite`へ切り替えるか、Snowflakeならクラスタリングキーで分割します。
- 全件再構築できなくなる:何ヶ月も増分だけ動かしていると、累積された不整合に気付かない。月1回など定期的に`–full-refresh`で再構築する運用を組み込みます。
いつincrementalに切り替えるべきか
incrementalは強力ですが、複雑さを増やします。最初から全モデルをincrementalにする必要はありません。判断軸はシンプルです。
| 状況 | 選ぶ材料 |
|---|---|
| テーブルが小さい(〜数百万行) | tableで十分 |
| full-refreshが10〜30分以上かかる | incremental検討の合図 |
| 毎日のデータ増分が全体の〜数% | incrementalの効果が大きい |
| 毎日大量の更新がある | mergeコストが膨らむ。設計慎重に |
「重くなってから切り替える」で問題ありません。先回りで全モデルをincrementalにすると、運用が複雑化し、不具合の原因も探しにくくなります。実装と運用のコストを比較し、痛みが出てから移行するのが結果的に楽です。
まとめ
- incremental modelsは「初回は全件、以降は差分だけ追記」で、変換時間とコストを大幅に削減できる。
- unique_keyと`is_incremental()`の組み合わせが基本。Snowflake/BigQueryではmerge戦略が既定。
- 遅延データには「過去N日まで遡って再評価」の窓を入れる。
- `on_schema_change`を明示し、新カラム追加に強くする。
- 定期的な`–full-refresh`を運用に組み込み、累積不整合を防ぐ。
テスト設計はdbt tests 実践、Snowflakeでのコスト最適化はdbt + Snowflake コスト最適化、履歴管理に踏み込むならdbt snapshots で SCD Type 2もあわせてどうぞ。dbt導入や運用改善の壁打ちは、DE-STKの初回相談(30分・無料)もご利用ください。
よくある質問(FAQ)
Q. incrementalにしたら、過去データの修正はどう反映しますか?
A. 通常の増分実行は新しいレコードしか取り込まないので、過去データの修正は反映されません。反映したい場合は、該当モデルを`–full-refresh`オプション付きで実行し、全件を作り直します。あるいは、遅延データ対策として「過去N日を毎回見直す窓」を入れておけば、その範囲内なら通常実行でも修正が反映されます。
Q. unique_keyに複数カラムを指定できますか?
A. 指定できます。`unique_key=[‘order_id’, ‘product_id’]`のようにリストで渡せば、複合キーとして扱われます。注文明細のように「注文番号+商品コード」で一意になるテーブルでは、複合キーが現実的な選択です。複合キーを使うと内部のSQLが複雑になるので、可能なら単一キーになるよう先に整形しておくのも手です。
Q. ソースから論理削除されたレコードはどう扱いますか?
A. 通常のincrementalでは、削除は伝播しません。「削除フラグ」のあるソースなら、フラグも含めて取り込み、ダウンストリームでフィルターするのが安全です。物理削除を反映する必要があるなら、`delete+insert`戦略か、定期的な`–full-refresh`を組み合わせます。ソース側の削除ポリシーを把握しないまま増分運用を始めると、いつまでも消えない幽霊レコードが残ります。
Q. Data VaultのSatelliteもincrementalで書けますか?
A. はい、Satelliteの差分検知ロジック(hash_diffが変わったときだけ追加)はincrementalモデルで書けます。実装パターンはData Vault 2.0 入門とData Vault on Snowflakeにまとめています。AutomateDVのようなパッケージを使えば、Satellite用の標準マクロが用意されており、incrementalの設定もパッケージ側で適切に組まれます。