unique_key は dbt の増分モデルで更新同一性を担保する要の設定です。ソース由来の重複を安全に排除しつつ、正しく MERGE(行差分更新)するための設計を丁寧に詰める必要があります。
本稿では、試験(dbt Analytics Engineer)で問われやすい概念と、現場での落とし穴を避けるための実務的ガイドラインを、安定した公式仕様に基づいてまとめます。
dbt の unique_key は、主に増分モデルでターゲット表の既存行と新規行を突き合わせるための「同一性カラム(群)」を指します。データベース上の主キー制約そのものを作るわけではなく、増分ロード時の MERGE 条件として機能します。
unique_key は行の一意性を保証するために使うため、自然キー、サロゲートキー、複合キーのいずれでも指定可能です。重要なのは、同一性の定義が業務上ブレず、かつ更新時のタイブレーク(どの行を最新とみなすか)が決められていることです。
dbt の増分モデルで incremental_strategy=merge を使う場合、unique_key の適切な指定が前提になります。これにより、再取り込みや訂正データの反映が idempotent(同じ結果に収束)になります。
重複排除と増分マージの流れ(概念)
増分モデルで unique_key を使った安全な去重 + MERGE(方言依存度の低い書き方)
{{ config(
materialized='incremental',
incremental_strategy='merge',
unique_key='order_id', # 複合キーの場合は ['order_id','line_no'] など
on_schema_change='append_new_columns'
) }}
with src as (
select * from {{ source('raw', 'orders') }}
{% if is_incremental() %}
-- 直近のみを取り込む例。適切な境界列(例: updated_at)を使う
where updated_at > (select coalesce(max(updated_at), '1970-01-01') from {{ this }})
{% endif %}
),
-- タイブレーク(最新行の決定)
ranked as (
select
*,
row_number() over (partition by order_id order by updated_at desc) as rn
from src
),
dedup as (
select * from ranked where rn = 1
)
select
order_id,
customer_id,
status,
total_amount,
updated_at
from dedup;重複排除は、一般に staging 層で行うと下流全体の安定性が増します。stg_* で「1ビジネスキー=1行」を保証しておけば、core/fact 層の増分モデルはシンプルに保てます。
ただし、CDC の遅延・再送が頻繁な系では、fact 側でもタイブレークを念押しする二重防御が有効です。stg での去重と、fact の MERGE 双方で order_id などの一意性が矛盾しないことを確認してください。
stg での去重(再利用しやすい共通パターン)
{{ config(materialized='view') }}
with base as (
select * from {{ source('raw', 'orders') }}
), ranked as (
select
*,
row_number() over (
partition by order_id
order by coalesce(updated_at, created_at) desc
) as rn
from base
)
select
order_id,
customer_id,
status,
total_amount,
coalesce(updated_at, created_at) as last_modified_at
from ranked
where rn = 1;incremental_strategy=merge は、多くのアダプタで unique_key を必須とし、同一キーの行を更新・挿入します。訂正や遅延到着データに強いのが利点です。
パーティション単位の再作成(例: insert_overwrite)を使う戦略では、unique_key よりも partition_by の設計が要となります。ビッグテーブルで日次再作成が現実的な場合に有効です。
append 戦略は単純追記で高速ですが、重複制御や訂正反映を自力で行う必要があります。去重の SELECT を確実にしないと重複が残ります。
戦略別の代表例(BigQuery 例含む)
-- 1) MERGE 戦略(多くのアダプタで一般的)
{{ config(materialized='incremental', incremental_strategy='merge', unique_key=['order_id','line_no']) }}
select * from {{ ref('stg_orders_dedup') }}
-- 2) パーティション単位の再作成(BigQuery の insert_overwrite)
-- パーティションキーは日付/時刻列など安定列を指定
-- MERGE ではないため unique_key は不要
{{ config(
materialized='incremental',
incremental_strategy='insert_overwrite',
partition_by={'field': 'order_date', 'data_type': 'date'}
) }}
select * from {{ ref('stg_orders_dedup') }}
where order_date >= date_sub(current_date(), interval 7 day);自然キーは業務上のキー(例: order_id)をそのまま使う方式で、可読性が高い一方、運用上の再利用や再採番変更に弱い場合があります。
サロゲートキーは安定した技術的キー(ハッシュや連番)で、複数列の組合せや NULL の扱いに強いのが利点です。変化に強いモデルを作りたいときに有効です。
複合キーは複数列で一意性を担保します。dbt の unique_key は配列指定で対応可能です。NULL の混入や列の将来変更に注意し、テストで堅く守るのが現実的です。
| キー種類 | 一意性の源泉/強み | 変更影響・実装の勘所 |
|---|---|---|
| 自然キー(単一列) | ビジネス上の識別子で直感的 | 上流の再採番・仕様変更に弱い。not_null/unique テスト必須 |
| サロゲートキー(ハッシュ/連番) | スキーマ変更や複合一意を吸収 | 生成ロジックの安定化と衝突回避。NULL 正規化が鍵 |
| 複合キー(複数列) | 現実の一意性を表現しやすい | 将来の列追加・削除に備えテスト強化。ユーティリティでハッシュ化も有効 |
dbt_utils を使ったサロゲートキー生成(複合キー対応)
-- packages.yml で dbt-labs/dbt_utils を追加しておく
-- モデル内での利用例
select
{{ dbt_utils.generate_surrogate_key([
'order_id',
"cast(line_no as string)",
"coalesce(customer_id, 'UNKNOWN')"
]) }} as sk_order_line,
*
from {{ ref('stg_orders_dedup') }};dbt のスキーマテストで not_null と unique を張るのが基本です。複合一意は dbt_utils.unique_combination_of_columns を使うと堅牢です。
一部のデータウェアハウスでは、主キー・一意制約が情報的で実行時に強制されない場合があります。そのため、ビルド時の dbt テストで実効性を担保するのが安全策です。必要に応じてクエリベースの監視(異常値検出)も併用してください。
大規模テーブルでは、全表スキャンのテストが重くなる場合があります。増分テストや、日次パーティション限定の検査など、運用パターンを工夫しましょう。
schema.yml の例(単一・複合一意のテスト)
version: 2
models:
- name: fct_orders
tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns: ['order_id', 'line_no']
columns:
- name: order_id
tests:
- not_null
- name: line_no
tests:
- not_null
- name: sk_order_line
tests:
- unique遅延到着や再送がある系では、去重のタイブレーク列(updated_at など)を必ず持ち、増分モデルでは MERGE を選択します。is_deleted フラグや version 列がある場合は、それらもタイブレークの材料になります。
CDC では、キーが重複しても after イメージが最後に来るとは限りません。row_number の order 基準を厳密にし、必要ならば same-key で最大 version を取るか、op_ts(操作時刻)を使います。
dbt スナップショットは、モデルの unique_key とは別に、スナップショット自身の unique_key 設計が必要です。SCD Type 2 的な履歴を作る場合、ビジネスキーで一意にしつつ、変更検出(check_cols など)を正しく指定します。
dbt スナップショットの例(SCD2)
{% snapshot dim_customer_scd %}
{{ config(
target_schema='snapshots',
unique_key='customer_id',
strategy='check',
check_cols=['name','email','status']
) }}
select * from {{ ref('stg_customers_dedup') }}
{% endsnapshot %}Analytics Engineer
問題 1
BigQuery 上の fact_orders を dbt の増分モデルで構築している。上流が再送・訂正を行うため、同一 order_id の行は最新更新だけを残し、既存行は正しく上書きしたい。最も適切な設定/実装の組み合わせはどれか?
正解: B
訂正・再送を安全に反映するには MERGE 戦略と unique_key が必要で、SELECT 側でも同一キーの最新行だけを残す去重(row_number 等)が要件です。partition_by だけの追記(A)や append(D)では上書きされず重複が残りやすく、常時フルリフレッシュ(C)は不要なコストが高すぎます。
dbt の unique_key とデータベースの主キー制約は同じですか?
異なります。dbt の unique_key は増分モデルの MERGE 条件として使われる論理的な同一性定義で、データベースの制約を必ずしも作成・強制しません。制約はウェアハウスごとに挙動が異なるため、dbt のテストで実効性を担保するのが安全です。
複合キーはどう指定すればよいですか?
多くのアダプタで unique_key に配列(例: ['order_id','line_no'])を指定できます。合わせて dbt_utils.generate_surrogate_key でハッシュを作ると扱いやすく、schema テストでは dbt_utils.unique_combination_of_columns を併用すると堅牢です。
遅延到着データで最新判定がぶれる場合の対処は?
去重クエリで決定的な順序を明示します。updated_at(なければ op_ts や version)に基づく row_number で rn=1 を採用し、is_deleted 等のフラグがある場合は優先度を設計します。必要なら複合条件で order by を組み、再実行しても同じ結果になるようにします。
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)、設定優先度...