株式会社renue
renueについて
renueは、AIで業務を実装する会社です
AIコンサルティングから図面AI・広告運用エージェント・コールセンターAIまで、実際に動いているサービスをご覧いただけます。renueが何をしている会社か、まずはサービス一覧からご確認ください。
dbt を導入した組織でしばらく経つと、「table マテリアライゼーションでは遅すぎる、全部 incremental にしたい」という声が出る。だが incremental には方式が複数あり、選定を誤ると重複行が積まれる、想定外のフルスキャンが走る、データが消えるといった事故が起きる。本記事では incremental の代表的な4方式 (append / merge / delete+insert / insert_overwrite) を、テーブル種別と data warehouse に紐づけて判定するフローを書く。公式にはmicrobatchもあり、対応方式はdbtのバージョンとアダプターで異なる(公式の対応表、2026年9月29日確認)。
代表的なマテリアライゼーション4種の使い分け — view / table / incremental / ephemeral
incremental に踏み込む前に、代表的な4つのマテリアライゼーションの住み分けを確認する。公式のMaterializationsにはmaterialized viewを含む5種類が載っており、利用可否や追加の方式はアダプター・バージョンにも依存する。
view: 軽量で常に最新だが、参照のたびに base クエリが走る。BI から直接参照される最終モデルには向かない。table: 毎回フルリビルド。データ量が小さく、ビルド時間が許容できる中間モデルの初期値として最適。incremental: 差分だけ追加更新する。データ量が大きく、フルリビルドの時間か warehouse コストが業務要件を破る段階で初めて検討する。ephemeral: CTE に展開される中間ロジック。深い依存で SQL が肥大化するので、参照階層は浅く保つ。
大事なのは「incremental は早すぎる最適化になりやすい」という事実。table でビルドが数秒のうちは incremental に切り替えない、と決めておくと事故が減る。
append 方式 — 重複を自動では除去しない前提で組む
最もシンプルな方式。{% if is_incremental() %} ブロック内で「last_updated > (SELECT MAX(last_updated) FROM {{ this }})」のような条件を使って入力を絞り、appendで追加する。ただし、この条件だけでは同じ更新時刻の後着行や遅延行を落とし、空の宛先ではMAXがNULLになって何も選べない。公式例のCOALESCE・同時刻を含む比較やlookbackを参考に抽出条件を設計する。append自体は既存行との照合を行わないため、再読込するなら別途重複を防ぐ必要がある。
append が向くのは、append-only な event ストリームに限る。clickstream / アプリログ / IoT センサーデータのような、行が一意に確定する性質のデータ。逆に上流のソースで何かのリトライが起きるとそのまま二重 insert になるため、必要な場合は最終BI出力側等で QUALIFY ROW_NUMBER() OVER (PARTITION BY event_id ORDER BY last_updated DESC, ingestion_sequence DESC) = 1 のように重複を除く。これはevent_idを業務上の一意なイベントID、ingestion_sequenceを同時刻の優先行を一意に決める取込連番とした例であり、実際のキーと採用順に置き換える。QUALIFYには真偽条件が必要で、行を分けるキーのPARTITION BY、採用順のORDER BY、1行を残す条件を省かない。
merge 方式 — 変更が起きる dimension テーブルの定石
upsert (insert or update) が必要な dimension テーブル向けの方式。unique_key を指定すると MERGE INTO で既存行を更新する。dbt Developer Hub「unique_key」は「unique_key が "old" 側に存在しない場合は INSERT、存在する場合は UPDATE/REPLACE」と挙動を定義している。
merge には2つの落とし穴がある。1つ目、incremental_predicates は宛先の走査範囲を絞る手段だが、未指定なら必ず全走査になるとは限らない。パーティション条件やMERGE条件によるpruning等で実際の走査範囲は変わる。既存行との一致を妨げるpredicateは二重挿入を招き得るため、遅延更新も含めて条件を確認する。Adrienne Vermorel 著「dbt Incremental Models: The Complete Guide」(2024年公開, adriennevermorel.com) は、行数が一定規模を超えると merge の性能劣化が顕著になると整理しており、預ける側で predicate を入れる重要性を強調している。2つ目、unique_key はモデルの行の粒度を表す必要があり、コスト削減のために一意性を落としてはならない。例えば user_id, snapshot_date の複合はユーザー別日次履歴なら候補になるが、ユーザー1行のdimensionとは粒度が異なる。自然キーを選ぶだけでcardinalityやコストが下がるわけではなく、キーの一意性・NULLと入出力の行数、走査量やクラスタリングを確認する。
delete+insert 方式 — 入力に対応するキーの行を置換する選択
標準的には入力データに現れるunique_keyに対応する既存行を削除し、入力行を挿入する方式である(公式のdelete+insert説明)。過去N日を置換するには、日付を置換キーとする設計や削除条件と、対象範囲の完全な入力が必要になる。入力にない日付の既存行まで自動的に削除するわけではない。DELETEとINSERTの2文という点だけからトランザクションが分かれるとは判断できない。dbt Developer Hub「About incremental strategy」は Snowflake / Databricks / Postgres でこの strategy をサポートしている。
atomicityはアダプター、バージョン、テーブルの種類と実行されるSQLで確認する。DELETEとINSERTという二つの処理があるだけで、別々のトランザクションとして確定するとは判断しない。検証環境で生成SQL、トランザクション境界、途中失敗時の残存データを調べ、復旧手順を試す。
insert_overwrite 方式 — BigQuery 上の fact テーブルの第一候補
dbt 公式の対応表ではBigQuery / Spark / Databricks等に対応するが、同じ方式名でも挙動は異なる。BigQueryではpartition全体を置換する一方、Snowflakeのinsert_overwriteはテーブル全体を置換するため、他のアダプターへそのまま移せない。BigQuery のpartitioned tables 公式ドキュメント (Google Cloud)で前提となる partition 設計と組み合わせて初めて性能を発揮する。
BigQueryのinsert_overwrite は unique_key を使わず、partition 単位で動く。これは行レベル比較を完全に回避できるという意味で、event log / 売上明細 / IoT メトリクスのような fact テーブルで最も性能とコストが見合う。バックフィルでは置換対象partitionの完全なデータを入力に含める必要があり、不足すれば既存行が失われる。入力内の重複は自動で除かれず、そのまま出力されるため、置換範囲・空partitionの扱い・入力の一意性を確認する(BigQueryの置換SQLとpartition指定)。Paradime「Optimizing dbt Incremental Models for Performance」(paradime.io) も BigQuery では insert_overwrite をデフォルト推奨として位置付けている。
方式選定フローチャート — テーブル種別から決める
テーブルが何を表現するかから方式を逆引きする。
- append-only な event ストリーム (clickstream / IoT / アプリログ):
append(対応アダプターの場合) またはinsert_overwrite(BigQuery等ではpartition設定と対象範囲の完全な入力が必要) - 変更が起きる dimension (users / accounts / org_tree):
mergeを基本候補とし、照合範囲を欠落させない条件でincremental_predicatesを検討 - 期間まるごと差し替えたい fact (日次集計 / 月次レポート): BigQuery なら
insert_overwrite、Snowflake なら日付キー・削除条件と完全な入力を設計したdelete+insert - window 関数で前後行を参照する集計 (cohort / retention): いったん
tableで完成させてから、必要ならincremental + lookbackで組み替える
バックフィルと late-arriving facts — 「気づいたら過去データがズレている」を防ぐ
incremental モデルが運用 1年も回ると、上流の修正・遅延データ・スキーマ変更で「過去データが本来の値からドリフトしている」事象が起きる。dbt 公式は「Incremental models in-depth」でこの問題を「late-arriving facts」と呼び、「定期的な full-refresh の運用」を推奨している。
具体的には、incremental のサンプルウィンドウに lookback (例: 直近7日) を持たせて、その期間内に遅延データが来ても拾える設計にする。appendでは再読込分が重複するため、mergeやpartition置換等の方式と組み合わせる。遅延分布と費用に応じ、例えば月1か四半期1で --full-refresh を打って、累積したドリフトをリセットする運用を仕込む。BigQueryのinsert_overwrite なら、対象partitionの完全なデータを再計算するバックフィルができる。これはテーブル全体を再構築するfull-refreshオプションとは別であり、コストは再計算範囲等で変わる。
スキーマ変更時の挙動 — カラム追加で過去行が NULL になる
incremental モデルにカラムを追加すると、過去行ではそのカラムが NULL になる。dbt 0.21 以降は on_schema_change: append_new_columns を設定すると、次回ビルドから新カラムを反映するが、過去行の値は自動では埋まらない。公式は手動UPDATEまたはfull-refreshを案内しており、--full-refresh だけが方法ではない。スキーマ進化のタイミングで「いつ、どの方法で過去行を補完するか」をチーム内で合意しておかないと、BI 側で気づいた時には数ヶ月分の NULL が積み上がっている。
モニタリング — 「incremental が効いていない」典型サイン
運用に入ったあとに観測すべきシグナル。
- ビルド時間が
--full-refreshと変わらない: incremental predicate が効いていない可能性。WHERE 句のカラムが partition と一致しているか確認 - 行数が想定より増えない: 上流側で「更新」されているのに、predicate が「新規行」しか拾えていない。merge への移行か、lookback の追加を検討
- merge cost が突発的に高騰: 入力・宛先の走査量、キーの一意性、partitionやクラスタリング、更新対象列を確認する。コストだけを理由に行の粒度を表すキー列を外さない。BigQuery なら slot 使用量、Snowflake なら credit 使用量で気づく
BigQueryでの方式選定と検証
BigQueryのfactテーブルでは、更新対象のpartitionを完全に再計算できるかを先に確認する。その条件を満たす場合にinsert_overwriteを候補とし、merge等と走査量・実行時間・再処理の負荷を比較する。テーブル種別だけで最適な方式を固定しない。
外部 API の繰り返し呼び出しでクラウド課金が膨らむ場合、結果をキャッシュとして保持し直す設計が有効になることがある。incremental モデルが目指す「同じ計算を二度しない」の発想は、データウェアハウス内のクエリだけでなく、データソース取り込みのレイヤーまで貫通する設計判断につながる。
選定理由を schema.yml に書き残す — 3か月後の自分を救うドキュメント
incremental の方式選定は、半年経つと「なぜこの選択だったか」を本人も忘れる。schema.yml の model description に「fact_orders は売上明細で日次 partition × insert_overwrite。mergeとの比較で、対象partitionを完全に再計算する方式を選んだ理由と、検証時の走査量・実行時間を記録する」のようにコメントとして残しておくと、新メンバーが触る時の事故が減る。dbt Developer Hub の incremental best practices も「choosing the right materialization is one of the most consequential decisions」と強調しており、この記録が運用品質の差を生む。
まとめ — incremental は「table で困ってから」始める
incremental は強力だが、運用コストもそれなりに伴う。table ビルドが業務要件を超えてから検討する、というルールを最初に置く。検討する時は本記事のフローで方式を決め、選定理由を schema.yml に残す。これだけで、データチームが3年後も触れる schema が残り、新メンバーの onboarding コストも下がる。
