incremental:速い更新と lookback の trade-off
Stage 3 — 第5章 | dbt入門カリキュラム 推定学習時間:40〜50分 | 難易度:★★★★☆
この章で学ぶこと
第4章 で日次 Activity mart を学びました。 多くの mart は full refresh 可能な table ですが、行数が増えると build 時間と warehouse コストが問題になります。
dbt の incremental モデル は、2 回目以降の build で差分のみ更新します。 ただし lookback 期間と full-refresh 手順を設計しないと、静かに古い数字が残る リスクがあります。
まず dbt の作法を整理し、続けて DS Playground の fct_activity_events_daily_by_type を読み解きます。
この章を終えると、こんなことができるようになります:
- incremental モデルの config ブロックを読める
- なぜ 14 日 lookback が必要か説明できる
unique_keyとdelete+insertの関係を理解できる- ソース遅延・バックフィル時の運用判断を説明できる
なぜ incremental か
| 方式 | 動き | 向いているケース |
|---|---|---|
| table(full) | 毎回全期間再計算 | 中小規模、ロジック変更が多い |
| incremental | 差分のみ更新 | 大規模 fact、遅延到着、コスト削減 |
テーブル・ビュー・MV で学んだ物理化の選択が、dbt では materialized config になります。
incremental を選ぶ典型理由は 3 つです。
- fact 行数が大きい … 毎回 full refresh すると warehouse コストが高い
- ソース遅延到着 … 過去日の行が後から修正・追加される
- 定期バッチの SLA … nightly build を短時間で終えたい
incremental は速さのための 近道 ですが、trade-off を理解した上で導入します。
is_incremental() = false"] B["2 回目以降
lookback 内のみ再集計"] C["delete+insert
by unique_key"] D["lookback 外は
触らない"] A --> C B --> C --> D
incremental config の読み方
{{
config(
materialized='incremental',
unique_key='...',
incremental_strategy='delete+insert',
on_schema_change='sync_all_columns'
)
}}
| 設定 | 典型値 | 意味 |
|---|---|---|
materialized |
incremental |
2 回目以降は差分 build |
unique_key |
grain に一致するキー | 行の一意識別子 |
incremental_strategy |
delete+insert |
対象キーを削除してから再 INSERT |
on_schema_change |
sync_all_columns |
列追加時の CI 挙動 |
unique_key は grain と一致 させる必要があります。
grain が 1 日 × 1 activity_type なら、キーも (date_day, activity_type) の合成列にします。
lookback の設計
incremental run では is_incremental() ブロック内で 直近 N 日だけ を upstream から読みます。
{% if is_incremental() %}
where date_day >= (
select coalesce(max(date_day), current_date) - interval 'N days'
from {{ this }}
)
{% endif %}
lookback を設ける理由:
- ソース遅延 … 同期やアプリ修正で過去日の行が更新される
- current-state テーブルの影響 … イベント履歴ではないソースでも、日付列の修正は起きうる
- 安全マージン … 運用上のバッファ(7 日では足りないケースをカバー)
lookback 内のキーは delete+insert で 上書き されます。
lookback より古い日 は incremental run では 触られません — ここが full-refresh が必要になる境界です。
--full-refresh が必要なとき
| シナリオ | incremental のみ | full-refresh |
|---|---|---|
| 昨日の activity 修正 | lookback 内なら通常 build で OK | — |
| lookback 外の過去日修正 | 古い行が更新されない | 必要 |
| 新列追加(schema change) | sync_all_columns で対応可 |
場合により |
| ロジック変更(定義変更) | 要検討 | 推奨 |
incremental 導入時のチェックリスト:
- unique_key は grain と一致するか
- lookback はソース遅延より長いか
- full-refresh 手順を runbook に書いたか
- 定義変更時に historical 再計算が必要と README に書いたか
- singular test が incremental 後も通るか
fct_activity_events_daily_by_type
DS Playground では fct_activity_events_daily_by_type が、教学用の incremental mart 例になっています。
int_user_activity_events から 日 × activity_type で集計する mart です。
grain: 1 行 = 1 日 × 1 activity_type
モデル config
{{
config(
materialized='incremental',
unique_key='activity_daily_key',
incremental_strategy='delete+insert',
on_schema_change='sync_all_columns',
tags=['product_health', 'active_users', 'incremental_example', 'no_pii']
)
}}
| 設定 | 値 | 意味 |
|---|---|---|
materialized |
incremental |
2 回目以降は差分 build |
unique_key |
activity_daily_key |
行の一意キー(date_day:activity_type) |
incremental_strategy |
delete+insert |
対象キーを削除してから再 INSERT |
on_schema_change |
sync_all_columns |
列追加時の CI 挙動 |
14 日 lookback の SQL
with activity_events as (
select *
from {{ ref('int_user_activity_events') }}
{% if is_incremental() %}
where date_day >= (
select coalesce(max(date_day), current_date) - interval '14 days'
from {{ this }}
)
{% endif %}
),
daily_activity_by_type as (
select
date_day,
activity_type,
count(*) as activity_records,
count(distinct user_id) as active_users,
min(occurred_at) as first_activity_at,
max(occurred_at) as last_activity_at
from activity_events
group by 1, 2
)
select
concat(date_day::text, ':', activity_type) as activity_daily_key,
...
from daily_activity_by_type;
なぜ 14 日か
- ソース遅延 — Supabase 同期やアプリ修正で過去日の行が更新される
- 最新状態テーブルの影響 —
problem_attemptsはイベント履歴ではないが、日付列の修正は起きうる - 安全マージン — 7 日では足りないケースをカバーする 運用上のバッファ
full-refresh が必要な場面
古い current-state ソース行が backfill または修正されたときは --full-refresh を使います。
30 日前の problem 状態修正は lookback 外のため、incremental のみでは 古い行が更新されません。
ロジック変更(定義変更)時も full-refresh が推奨されます。
CI では analytics_ci schema で build されるため、本番 analytics とは別です(Stage 5 第 3 章)。
出力列の読み方
| 列 | 用途 |
|---|---|
activity_records |
行動イベント件数(同一ユーザーの複数行動含む) |
active_users |
その type の distinct user |
first_activity_at / last_activity_at |
日次の時間範囲。異常検知に使える |
fct_active_users_daily の type 別列と 定義は近い が、grain が type 別行 のため Metabase pivot に向きます。
よくある誤解
| 誤解 | 実際 |
|---|---|
| 「incremental = 常に速い・常に正確」 | lookback 外の修正は残る。full-refresh が必要 |
| 「unique_key は飾り」 | delete+insert の対象行を決める |
| 「lookback を長くすれば万能」 | コスト増。遅延実績に合わせて設計 |
体系コラム:カリキュラム上の位置づけ
| 項目 | 内容 |
|---|---|
| Stage / 章 | Stage 3 — 第5章 |
| 今回の論点 | incremental config、14 日 lookback、delete+insert、full-refresh |
| 前章との接続 | Active User mart |
| 次章への伏線 | Monetization status — Stage 4 |
まとめ
incremental は差分更新で build 時間とコストを抑えますが、lookback 期間と full-refresh 手順が設計の要です。
unique_key と grain を一致させ、ソース遅延より長い lookback を設け、定義変更時は historical 再計算を runbook に書きます。
DS Playground では fct_activity_events_daily_by_type が教学例として incremental を実践し、14 日 lookback + delete+insert で product health の type 別推移を返しています。
関連教材
- 前章: Active User mart
- 次章(Stage 4): Monetization status
- 関連(DM): テーブル・ビュー・MV
確認問題
問題 1
14 日 lookback の主な目的はどれですか。
A. PII を 14 日で自動削除する
B. 遅延到着・過去日修正を incremental 更新で吸収する
C. BI ツールのキャッシュ期限に合わせる
D. OLTP の課金を 14 日単位にする
正解: B
問題 2
30 日前の fact 行が修正されたとき、lookback なしの incremental build だけでは足りない理由はどれですか。
A. incremental は未来日しか処理しない
B. lookback 外の日は再集計されない
C. delete+insert は使えない
D. unique_key が無効になる
正解: B
問題 3
incremental_strategy='delete+insert' と unique_key を組み合わせた incremental mart で起きることとして正しいのはどれですか。
A. 表全体が毎回 TRUNCATE される
B. 対象キー行が削除され、lookback 内が再 INSERT される
C. 重複キーが許容される
D. view に変換される
正解: B
用語メモ(この章)
| 用語 | 意味(この章での使い方) |
|---|---|
| incremental | 差分のみ更新する materialization |
| lookback | incremental run で再集計する直近 N 日 |
unique_key |
delete+insert の対象行を決める一意キー |
delete+insert |
対象キー削除後に再 INSERT する戦略 |
is_incremental() |
2 回目以降の build かを判定する Jinja 関数 |
| full-refresh | 全期間を再計算する --full-refresh build |