dbt の Snapshot は、ソースの行が変化したときに履歴行を増やし、有効期間を管理することで SCD Type 2 を実現します。代表的な戦略は timestamp と check の2つで、どちらも unique_key が必須です。
本稿では、2つの戦略の選定基準、よくある落とし穴、倉庫別の実務チューニング、試験で狙われやすい論点を、公式ドキュメントの挙動に沿って解説します。
dbt の Snapshot は、SELECT 文の結果を基準に、行ごとの変化を検知して履歴テーブルを更新します。更新検知が発生すると既存の現行行の有効終了を閉じ、新しい現行行を追加します。結果として、同じ unique_key の行に複数バージョンが蓄積されます。
Snapshot テーブルには、dbt が付与する制御カラムが含まれます。これらは戦略に関わらず共通で、履歴の整合性検証や期間クエリに利用できます。
| カラム | 役割 | 補足 |
|---|---|---|
| dbt_scd_id | SCD 行の一意識別子 | unique_key と戦略に基づき dbt が算出 |
| dbt_valid_from | 行の有効開始時刻 | 変化が検知された時刻(または updated_at) |
| dbt_valid_to | 行の有効終了時刻 | 現行行は NULL、失効時にタイムスタンプが入る |
| dbt_updated_at | dbt が処理時に付与する更新基準 | 戦略により算出方法が異なる |
最小構成の Snapshot(check 戦略)
{% snapshot customers_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='check',
check_cols=['email', 'address', 'status'],
invalidate_hard_deletes=true
)
}}
select customer_id, email, address, status
from {{ source('app', 'customers') }}
{% endsnapshot %}timestamp 戦略は、単一の更新時刻カラム updated_at を基準に行の変化を検知します。updated_at が単調に増加し、全ての変更時に正しく更新されるデータセットに最適です。
check 戦略は、指定したカラム集合の値変化で検知します。更新時刻が信用できない場合や、ビジネス上の重要属性だけで変更判定したい場合に有効です。
| 観点 | timestamp 戦略 | check 戦略 |
|---|---|---|
| 検知ロジック | updated_at の増加で検知 | check_cols の値変化で検知 |
| 必須データ | 信頼できる updated_at | 安定かつ決定的な複数列(1列でも可) |
| 誤検知リスク | 時刻が遅延・欠落すると取りこぼし | 不要列を含むと過剰検知(ノイズ) |
| パフォーマンス | 比較的軽い(単一列参照) | 列数・比較負荷に依存 |
| ユースケース | CDC 由来の updated_at がある DWH テーブル | アプリ由来で updated_at 不整合、特定属性のみ追跡したい場合 |
Snapshot の履歴化イメージ(SCD2)
両戦略のスケルトン
{% snapshot orders_ts %}
{{
config(
target_schema='snapshots',
unique_key='order_id',
strategy='timestamp',
updated_at='updated_at'
)
}}
select order_id, updated_at, amount, status
from {{ ref('stg_orders') }}
{% endsnapshot %}
{% snapshot users_ck %}
{{
config(
target_schema='snapshots',
unique_key='user_id',
strategy='check',
check_cols=['email', 'is_active', 'plan']
)
}}
select user_id, email, is_active, plan
from {{ ref('stg_users') }}
{% endsnapshot %}updated_at はソース側で全更新時に必ず書き換わる必要があります。遅延到着やバックフィルで updated_at が過去を指すと、検知漏れの原因になります。信頼できる単調増加の時刻を確保してください。
タイムゾーンの取り扱いは倉庫依存です。ソースと DWH の時刻帯が不一致だとウィンドウ境界がずれます。ETL 側で UTC に正規化するか、Snapshot の SELECT 内で明示的に正規化しましょう。
| 事象 | 原因 | 回避策 |
|---|---|---|
| 検知漏れ | updated_at が遅延・逆行 | ETL で単調増加保証、遅延到着フラグで再投入 |
| 境界ずれ | タイムゾーン不一致 | UTC 正規化、SELECT で明示変換 |
| 無駄な履歴 | 機械的な定時更新で updated_at 変化 | アプリ側で意味的変更時のみ更新 |
SELECT 内での時刻正規化例(概念)
{% snapshot payments_ts %}
{{
config(
target_schema='snapshots',
unique_key='payment_id',
strategy='timestamp',
updated_at='updated_at_utc'
)
}}
select
payment_id,
-- ここで UTC に正規化(倉庫に応じて適切な関数を使用)
cast(updated_at as timestamp) as updated_at_utc,
amount, status
from {{ ref('stg_payments_raw') }}
{% endsnapshot %}check_cols は業務的に意味のある変更を反映する列に限定します。ログ由来の更新時刻や並び順、非決定的関数の結果は除外してください。浮動小数は丸め誤差で不要な変更検知の原因になりやすいので、比較前に丸めるか文字列化するなどの前処理が有効です。
派生列をチェックしたい場合は、Snapshot の SELECT で安定的に計算してから check_cols に含めます。これによりソーススキーマの変更影響を最小化できます。
| 事象 | 原因 | 回避策 |
|---|---|---|
| 過剰検知 | ノイズ列を含めた | check_cols を絞り込み、前処理を行う |
| 微小差で検知 | 浮動小数の丸め誤差 | round や cast、文字列化で安定化 |
| 検知漏れ | 比較に含めるべき列の漏れ | 要件から差分定義を棚卸して列を見直す |
check_cols を前処理して安定化する例
{% snapshot subscription_ck %}
{{
config(
target_schema='snapshots',
unique_key='account_id',
strategy='check',
check_cols=['plan', 'is_active', 'mrr_str']
)
}}
with src as (
select account_id, plan, is_active, mrr
from {{ ref('stg_subscription') }}
)
select
account_id,
plan,
is_active,
-- 丸めと型統一により比較を安定化
cast(round(mrr, 2) as string) as mrr_str
from src
{% endsnapshot %}Snapshot は実行のたびに差分を評価します。データ更新頻度に合わせたスケジュールが重要で、過剰な頻度は不要な比較コストを招きます。逆に低すぎると検知タイムラグが伸びます。
履歴テーブルは時間とともに増大します。クエリ対象が現行行のみなら、dbt_valid_to が NULL の行を参照するビューを用意し、パーティションやクラスタリングで読み取りを最適化します。
| 倉庫・観点 | 推奨設定例 | 狙い |
|---|---|---|
| 共通(スケジュール) | 更新直後に1回、必要なら時間帯を限定 | 無駄な差分比較を削減 |
| 共通(ストレージ) | dbt_valid_from/valid_to で分割・クラスタ | 時間順クエリの I/O 削減 |
| 共通(ビュー) | 現行行ビュー: where dbt_valid_to is null | 大半の参照を軽量化 |
現行行のみを返すビュー例
create or replace view analytics.v_customers_current as
select *
from snapshots.customers_snapshot
where dbt_valid_to is null;Analytics Engineer 試験では、timestamp と check の適用条件、unique_key の必須性、ハードデリートの扱い、比較対象列の選定などが頻出です。演算子や倉庫固有関数の知識よりも、意味的に正しい差分定義と安定した比較の考え方が問われます。
Snapshot は増分モデルとは目的が異なります。増分は最新状態を効率更新する手段、Snapshot は履歴を保持する手段です。この違いを混同しないでください。
| 項目 | 覚えるポイント | ひっかけ例 |
|---|---|---|
| unique_key | 全スナップショットで必須 | 自然キーが揺れる設計 |
| timestamp | 信頼できる updated_at が前提 | 遅延到着で検知漏れ |
| check | 業務的に重要な列だけ比較 | ログ列や更新時刻を含める |
schema.yml でのテスト例(unique と not_null)
version: 2
snapshots:
- name: customers_snapshot
tests:
- not_null:
column_name: customer_id
- unique:
column_name: customer_idAnalytics Engineer
問題 1
オンライン注文テーブルを Snapshot 化したい。アプリは金額変更時に amount と status は更新するが、updated_at は一部のバックフィルで過去時刻に上書きされることがある。誤検知を抑え、取りこぼしも避けたい。どの構成が最も適切か。
正解: A
updated_at が信頼できないため timestamp は不適。check で業務的に意味のある列(amount, status)に限定し、金額は丸め誤差を避ける前処理を入れるのが妥当。全カラム比較はノイズ増、現在時刻を使うのは誤検知の温床。
Snapshot と増分モデル(incremental)の違いは何ですか?
増分モデルは最新状態のテーブルを効率的に更新する手法で、履歴は保持しません。Snapshot は変更のたびに旧行の有効終了を閉じ、新行を追加して履歴を保持します。用途が異なります。
ソースから行が物理削除された場合、Snapshot は自動で行を失効させますか?
デフォルトでは失効しません。invalidate_hard_deletes=true を有効にすると、スナップショット実行時にソースに存在しない行を検出し、dbt_valid_to を更新して失効させられます。
check_cols に計算列を含めても安全ですか?
決定的で安定した計算なら安全です。非決定的関数(現在時刻、乱数)や丸め誤差の出る浮動小数は避け、必要に応じて round や cast、coalesce で前処理してください。
NicheeLab編集部
データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。
dbt Model の基礎: SQL で定義する変換の最小単位
Analytics Engineer 向けに、dbt Model の定義、マテリアライゼーション、依存関係、インクリメン...
dbt Analytics Engineer 試験ガイド: 出題範囲・配点・申込の実務視点
dbt Analytics Engineer 認定の出題範囲、配点の考え方、申込から受験までの流れを、公式ドキュメントの...
dbt Cloud と dbt Core の違いと選び方:Analytics Engineer 試験に効く要点
dbt Cloud と dbt Core の機能差を、実務と資格対策の両面から整理。スケジューリング、IDE、RBAC、...
dbt プロジェクト構造ガイド: models / seeds / macros の実務レイアウト
Analytics Engineer 向けに、dbt プロジェクトのディレクトリ構造と命名規約、dbt_project....
dbt_project.yml の読み方:主要設定と命名を最短で掴む
dbt_project.yml の必須キー、命名解決(database.schema.identifier)、設定優先度...