---
title: "dbt snapshotの使い方：timestamp・check戦略でSCD Type2の履歴を作る手順【1.12対応】"
url: "https://www.issoh.co.jp/tech/details/19613/"
published: 2026-11-11
updated: 2026-11-11
categories: ["データベース"]
publisher: "株式会社一創"
---

# dbt snapshotの使い方：timestamp・check戦略でSCD Type2の履歴を作る手順【1.12対応】

dbt snapshotは、上書き更新されるテーブルの「過去の状態」を行ごとに残すdbtの機能です。顧客のランクや注文のステータスが変わったとき、変更前の行に終了時刻を入れ、新しい行を追加していきます。この形はSCD Type2と呼ばれます。本記事では、dbt Core 1.12系（2026年11月時点の最新は1.12.6）を前提に、1.9以降のYAMLでの定義、timestamp戦略とcheck戦略の選び方、削除行の扱い、下流モデルでの時点指定の結合までをコード付きでまとめました。最後に、既存スナップショットの移行手順と、dbt snapshotを使わないほうがよい場面も書いています。

## まとめ：dbt snapshotをYAMLで定義しtimestamp戦略で日次運用

新しく作るなら、`snapshots/`配下のYAMLに`relation`・`unique_key`・`strategy`を書く方式（dbt 1.9以降）を使います。信頼できる更新日時の列があればtimestamp戦略、無ければ比較する列を明示したcheck戦略が選択肢です。`check_cols: all`は手軽ですが、公式も列の明示を勧めています。

運用で押さえることは2つです。スナップショットは作り直せません。`--full-refresh`も無視されるため、別スキーマに置き、`unique_key`が本当に一意かをテストで守ります。実行は1時間〜1日に1回が想定範囲で、それより細かい変更を拾いたいならCDCなど別の手段を検討します。

## dbt snapshotが作るSCD Type2の履歴テーブルとメタ列の読み方

最初に、スナップショットがどんな形のテーブルを作るのかを押さえます。書き方は、出来上がる表の形から逆算すると迷いません。

### ステータスが2回変わった注文の具体例で見る履歴行の増え方と終了時刻

注文ID 1のステータスが pending から shipped に変わったとします。元テーブルは上書きされて shipped の1行しか残りませんが、スナップショット側には次の2行が残ります。

| order\_id | status  | dbt\_valid\_from | dbt\_valid\_to   |
| --------- | ------- | ---------------- | ---------------- |
| 1         | pending | 2026-11-01 10:47 | 2026-11-02 09:05 |
| 1         | shipped | 2026-11-02 09:05 | NULL             |

変更前の行は`dbt_valid_to`に終了時刻が入って「無効」になり、変更後の行は`dbt_valid_to`がNULLのまま「現行」として残ります。ある時点の状態を知りたければ、その時刻が`dbt_valid_from`以上・`dbt_valid_to`未満に入る行を選べば済みます。

### 履歴の有効期間・行の識別・変更判定を担う4つのメタ列の役割と読み方

dbtはスナップショットに4つのメタ列を自動で足します。[公式ドキュメント（Add snapshots to your DAG）](https://docs.getdbt.com/docs/build/snapshots)の定義を整理すると次のとおりです。

| 列                | 中身                 | 主な使い道           |
| ---------------- | ------------------ | --------------- |
| dbt\_valid\_from | その行が有効になった時刻       | 時点指定の結合の下限      |
| dbt\_valid\_to   | 無効になった時刻（現行行はNULL） | 現行行の抽出・結合の上限    |
| dbt\_scd\_id     | 行ごとに生成される一意キー      | dbt内部の照合・一意性テスト |
| dbt\_updated\_at | 挿入時点の元行の更新時刻       | dbt内部の変更判定      |

挿入時は`dbt_valid_from`と`dbt_updated_at`に同じ値が入ります。`hard_deletes: new_record`を設定したときだけ加わる5つ目の列が、削除を表す`dbt_is_deleted`です。列名を社内の命名規約に合わせたい場合は、[snapshot\_meta\_column\_names](https://docs.getdbt.com/reference/resource-configs/snapshot%5Fmeta%5Fcolumn%5Fnames)で`dbt_valid_from`を`start_date`のように読み替えられます。

## snapshots配下のYAMLにtimestamp戦略のスナップショットを定義する手順

ここからは手を動かします。環境構築（インストール・profiles.yml・接続確認）は[dbt Coreの使い方](https://www.issoh.co.jp/tech/details/19592/)で済ませている前提です。

### 検証環境の前提：dbt Core 1.12系とYAML定義が使える1.9以降の条件

YAMLでスナップショットを書く方式はdbt 1.9から使えます。1.8以前はSQLファイルに`{% snapshot %}`ブロックを書く旧方式で、既存プロジェクトにはこちらが残っていることがあります。[PyPIのdbt-core](https://pypi.org/project/dbt-core/)では、2026年11月11日時点の最新が1.12.6（10月14日公開）です。

```
python -m pip install dbt-core dbt-postgres
dbt --version
```

アダプタはDWHに合わせて dbt-snowflake や dbt-bigquery に読み替えてください。Snowflakeの接続設定は[dbtとSnowflakeの連携](https://www.issoh.co.jp/tech/details/19597/)に手順があります。スナップショットは既定で`snapshots/`ディレクトリに置き、モデルと同じディレクトリには同居させられません。

### 顧客マスタをupdated\_atで追跡するYAML定義と各設定の意味

CRMから取り込んだ`customers`テーブルに、信頼できる`updated_at`列がある例です。次のファイルを`snapshots/customers_snapshot.yml`として保存します。

```
snapshots:
  - name: customers_snapshot
    relation: source('crm', 'customers')
    config:
      schema: snapshots
      unique_key: customer_id
      strategy: timestamp
      updated_at: updated_at
```

`relation`には`source()`か`ref()`で追跡対象を書きます。`unique_key`は行を突き合わせるキーで、複合キーの場合は配列での指定も可能です。`schema`を省くと、1.9以降は`generate_schema_name`マクロで保存先が決まります。

timestamp戦略では、`updated_at`が前回より新しい行だけを「変わった」とみなします。`dbt_valid_from`に入るのは実行時刻ではなく元テーブルの`updated_at`の値なので、実行が30分遅れても履歴の時刻はずれません。

### dbt snapshotの実行と、生成SQL・履歴行を確かめるコマンド

定義を書いたら実行します。1回目は元テーブルの全行が現行行として入り、2回目以降は変わった行だけが追加されます。

```
dbt snapshot --select customers_snapshot
dbt compile --select customers_snapshot
```

`dbt compile`でスナップショットの生成SQLを`target/compiled/`に出せるのは1.12以降です。MERGEの条件を確かめたいときに使えます。コマンドの全オプションは[About dbt snapshot command](https://docs.getdbt.com/reference/commands/snapshot)に一覧があります。

2回目の実行後、元テーブルで1件だけステータスを書き換えてから3回目を回し、該当IDの行が2行になっていれば定義は動いています。

## updated\_atが無いテーブルにcheck戦略とcheck\_colsを設定する書き方

基幹システムから来るテーブルには、更新日時の列が無い、あるいは一部の更新で書き換わらないものがあります。その場合はcheck戦略を使います。

### check\_colsは列を明示し、allを避けて代理キーで比較列を束ねる

check戦略は、指定した列の現在値と履歴の値を比べて変更を判定します。

```
snapshots:
  - name: orders_snapshot
    relation: ref('stg_orders')
    config:
      schema: snapshots
      unique_key: order_id
      strategy: check
      check_cols:
        - status
        - is_cancelled
```

`check_cols: all`で全列を比較することもできますが、公式は比較したい列を明示するよう勧めています。全列比較では、取り込み時刻のような毎回変わる列まで拾うため、追跡対象の値に変更の無い行まで毎回増える原因になります。この増加は、比較する列の選び方によるものです。比較列が10を超えるなら、上流のモデルでそれらを1つの代理キー（ハッシュ値）にまとめ、その1列を`check_cols`に指定すると定義が短く保てます。

check戦略では、変更を検知した時刻として実行時刻が`dbt_valid_from`に入ります。日次で回すなら、履歴の時刻精度は「1日のどこかで変わった」までと割り切ってください。`updated_at`も併せて設定すると、その列の値が時刻に使われます（値がNULLの行は実行時刻）。

### 重複や不要行を除くephemeralモデルを挟んでから追跡する構成

元テーブルに同じキーの行が重複していると、スナップショットは正しく照合できません。`unique_key`の一意性について、公式は「extremely important」と書いています。重複除去や絞り込みが要るときは、ephemeralモデルを1枚挟みます。

```
{{ config(materialized='ephemeral') }}

select *
from {{ source('erp', 'orders') }}
qualify row_number() over (partition by order_id order by loaded_at desc) = 1
```

このモデルを`relation: ref('ephemeral_orders')`として参照すれば、スナップショットの定義は追跡の設定だけに保てます。`qualify`はSnowflakeやBigQueryの構文で、Postgresではサブクエリで同じ処理を書きます。

## hard\_deletesとdbt\_valid\_to\_currentで削除行と現行行を表す

元テーブルから行が物理削除されたとき、既定のスナップショットは何もしません。削除を履歴に残すかどうかは、1.9以降の`hard_deletes`で決めます。

### ignore・invalidate・new\_recordの3値で変わる削除行の残り方

[hard\_deletesの公式リファレンス](https://docs.getdbt.com/reference/resource-configs/hard-deletes)によると、指定できる値は3つです。

| 値           | 削除された行の扱い                  | 向く場面           |
| ----------- | -------------------------- | -------------- |
| ignore（既定）  | 変更なし。dbt\_valid\_toはNULL   | 削除が起きないテーブル    |
| invalidate  | dbt\_valid\_toに現在時刻を入れて無効化 | 削除後の空白期間を許容できる |
| new\_record | dbt\_is\_deleted=Trueの行を追加 | 削除と復活を履歴で追いたい  |

ignoreのままだと、削除された顧客がいつまでも「現行」に見えます。会員数の推移など件数を数える用途なら、削除された行を現行行として数え続けないよう、invalidateかnew\_recordを選ぶ方針です。new\_recordでは、同じキーの行が復活すると`dbt_is_deleted`がFalseの行が追加されます。旧設定の`invalidate_hard_deletes`は既存スナップショット向けに残っていますが、`hard_deletes`と同時には使えません。

### dbt\_valid\_to\_currentで現行行を9999-12-31にするときの制約

現行行の`dbt_valid_to`をNULLでなく遠い未来の日付にしておくと、時点指定の結合で`coalesce`が要らなくなります。

```
    config:
      strategy: timestamp
      updated_at: updated_at
      unique_key: customer_id
      dbt_valid_to_current: "to_date('9999-12-31')"
```

[dbt\_valid\_to\_currentのリファレンス](https://docs.getdbt.com/reference/resource-configs/dbt%5Fvalid%5Fto%5Fcurrent)には制約が2つあります。指定できるのは静的なSQL式だけで、`{{ var(...) }}`のようなJinjaは使えません。日付の書き方はDWHごとに異なります。もう1つは、既存のスナップショットに後から足しても、すでにNULLになっている現行行は書き換わらない点です。NULLと9999-12-31が混ざるので、揃えるには自分でUPDATEを流します。

## 下流モデルでスナップショットを使う最新行の抽出と時点指定の結合SQL

スナップショットは`ref('customers_snapshot')`で参照できます。使い方はほぼ2通りです。

### 注文時点の顧客ランクを履歴の有効開始時刻と終了時刻で結合するSQLの条件

最新の状態だけが要るなら`where dbt_valid_to is null`で現行行に絞ります。分析で効くのはもう1つの「注文した時点の顧客ランク」を引く結合です。

```
select
    o.order_id,
    o.ordered_at,
    c.customer_rank
from {{ ref('stg_orders') }} as o
left join {{ ref('customers_snapshot') }} as c
    on o.customer_id = c.customer_id
   and o.ordered_at >= c.dbt_valid_from
   and o.ordered_at < coalesce(c.dbt_valid_to, cast('9999-12-31' as timestamp))
```

上限側を「未満」にしているのは、切り替わりの瞬間に2行が当たるのを防ぐためです。`dbt_valid_to_current`を設定済みなら`coalesce`は外せます。この結合はmarts層のファクトを作る段で書くのが自然で、層の分け方は[dbtのmarts層の記事](https://www.issoh.co.jp/tech/details/12323/)で整理しています。

### キーごとの現行行が1件だけかを検査するスナップショット用テスト

スナップショットが壊れたかどうかは、「同じキーの現行行が2件以上ある」状態で分かります。原因となることが多いのは`unique_key`の重複で、放置すると上の結合で行が増える状態です。次のsingular testを`tests/`に置きます。

```
select customer_id
from {{ ref('customers_snapshot') }}
where dbt_valid_to is null
group by customer_id
having count(*) > 1
```

あわせて、YAMLで`dbt_scd_id`に`unique`のgeneric testを付け、元テーブル側の`customer_id`にも`unique`を付けておくと、重複が履歴に入る前に止められます。テストの書き方の全体は[dbt testの書き方](https://www.issoh.co.jp/tech/details/19611/)にまとめています。

## 既存スナップショットの移行と運用で履歴を壊さないための判断基準

ここからは、競合記事があまり触れていない「動いている履歴を守る」側の話です。スナップショットの失敗は後から取り返せない点で、モデルの失敗とは重さが違います。

### 作り直せない前提で別スキーマに置きfull-refreshを頼らない運用

モデルは元データから何度でも作り直せますが、スナップショットは過去の実行時点の状態を貯めたものです。テーブルを消せば、その期間の履歴は戻りません。公式も「Snapshots can’t be rebuilt」として、別スキーマに置くよう勧めています。

`dbt snapshot --full-refresh`は効きません。`full_refresh`設定もフラグも無視されるため、うっかり作り直す事故は起きにくい作りです。逆に、定義を大きく変えたいときも作り直しで逃げられません。元テーブルから列が消えてもスナップショット側の列は消えず、型も（varcharの桁拡張を除いて）変わりません。設計の初手で`unique_key`と戦略を固めておくのが、後の手戻りを最も減らします。

### 1.9の新設定を既存スナップショットに足すときのバックアップと検証順

`hard_deletes`・`dbt_valid_to_current`・`snapshot_meta_column_names`は、新規スナップショット向けの設定です。既存の表に足してもdbtは自動で移行しません。[Snapshot configurations](https://docs.getdbt.com/reference/snapshot-configs)の移行手順を要約すると次の順になります。

1. DWH上でバックアップを取る（`create table 〜_backup as select * from 〜`）
2. 新しい設定に必要な列を`alter table`で追加する
3. YAMLの設定を1つだけ変える
4. 開発・ステージング環境で`dbt snapshot`を流し、列と履歴の形を確かめる
5. 問題なければ次の設定に進む

移行せずに設定だけ変えると、旧形式と新形式の行が混ざり、下流の集計が黙って狂います。設定を変えたスナップショットは`state:modified`の選択対象にも入るので、CIで変更を検知したら本番の前に手作業の移行が要ると判断してください。

### 1時間より細かい変更の追跡と、dbt snapshotを使わない場面

公式は、スナップショットを1時間〜1日の間隔で回す想定だと書いています。バッチで差分を取る仕組みなので、実行と実行の間に2回変わった行は、最後の状態しか残りません。1日に何度も変わるステータスを全部追いたいなら、dbt snapshotは使いません。DBのログを読むCDCツールで変更を全件取り込み、dbtでは届いた履歴を整形する側に回ります。取り込みツール側の構成は[Fivetranの記事](https://www.issoh.co.jp/tech/details/15954/)で扱っています。

もう1つの見送り場面は、元テーブル自体が履歴を持っている場合です。会計の仕訳や在庫の入出庫のように行が追記されるだけのテーブルを追跡しても、同じ情報を二重に持つだけになります。どの表に履歴を持たせ、どこをCDCに任せるかは、データ基盤全体の設計で決める話です。設計から相談したい場合は[データ分析基盤構築・MLOps構築支援](https://www.issoh.co.jp/service/ai/data-platform/)で受け付けています。

## dbt snapshotの導入時によくある質問と戦略・実行・保存先の判断

dbt snapshotの導入時に出やすい質問に答えます。

### dbt snapshotとincrementalモデルの違いは何ですか？

目的が違います。incrementalモデルは、新しく届いた行や変わった行だけを処理して、大きなテーブルの再作成を省くための仕組みです。更新された行は上書きされるのが基本で、変更前の値は残りません。dbt snapshotは上書きを前提にしたテーブルから変更前の値を残すための仕組みで、行ごとに有効期間を持たせます。「処理を速くしたい」ならincremental、「過去の状態を知りたい」ならsnapshotと考えてください。

### dbt buildを実行するとスナップショットも一緒に実行されますか？

実行されます。`dbt build`はモデル・テスト・シード・スナップショットを、依存関係の順に実行します。スナップショットを`ref()`で参照するモデルがあれば、スナップショットのあとに処理される仕組みです。スナップショットだけを別の時間帯に回したい場合は、ジョブを分けて`dbt snapshot`を単独で実行し、`dbt build`側では`--exclude resource_type:snapshot`で外す運用もできます。

### スナップショットのテーブルはどのスキーマに作られますか？

dbt 1.9以降のYAML定義では、`config`の`schema`と`database`で指定します。省略した場合は、モデルと同じく`generate_schema_name`・`generate_database_name`マクロで決まります。開発者ごとにスキーマを分けているプロジェクトでは、本番のスナップショットを誤って開発用スキーマに作らないよう、`schema`を明示し、マクロの挙動も確認しておくと安全です。

### timestamp戦略とcheck戦略はどちらを選べばよいですか？

信頼できる更新日時の列があるならtimestamp戦略です。公式も、列の追加・削除に強い点からtimestamp戦略を推奨しています。更新日時の列が無い、またはバッチ更新で書き換わらない列がある場合にcheck戦略を選びます。迷ったら、元テーブルで更新日時が本当に毎回変わるかを、数日分のデータで確かめてから決めてください。

### 元テーブルに列を追加したらスナップショットにも反映されますか？

反映されます。元のクエリに新しい列が増えると、次の実行でスナップショットのテーブルにも列が追加されます。追加前の履歴行では、その列はNULLです。check戦略で`check_cols`を明示している場合、新しい列は比較の対象に入らないので、変更を追いたいなら`check_cols`にも足してください。エンジンをdbt Fusion（v2）へ移す予定があるなら、移行の手順は[dbt Fusionとは](https://www.issoh.co.jp/tech/details/19593/)を先に確認しておくと安心です。

## 関連記事

- [dbtとは？ELTのTを担う仕組みとdbt Core・Cloudの選び方【2026年版】](https://www.issoh.co.jp/column/details/16548/)：dbtの仕組みと採用判断
- [dbt Coreの使い方：インストールからprofiles.yml・dbt build・CIまでの手順【1.12対応】](https://www.issoh.co.jp/tech/details/19592/)：スナップショットを書く前の環境構築
- [dbt testの書き方：generic・singular・unit testの使い分けとCIへの組み込み【1.12対応】](https://www.issoh.co.jp/tech/details/19611/)：スナップショットに付けるテスト
- [dbtのmarts層とは？3層の分け方とMetricFlowで指標を定義する手順【dbt 1.12対応】](https://www.issoh.co.jp/tech/details/12323/)：履歴を結合する層の設計
- [データ契約（Data Contract）とは？ODCSとdbtでスキーマ破壊を止める実装を解説【2026年版】](https://www.issoh.co.jp/tech/details/16109/)：元テーブルの列変更を止める方法

---

出典: [dbt snapshotの使い方：timestamp・check戦略でSCD Type2の履歴を作る手順【1.12対応】](<https://www.issoh.co.jp/tech/details/19613/>)（株式会社一創）
