dbtのテストは大きくgeneric testとsingular testに分かれます。genericがパラメトリックに再利用する型であるのに対し、singularは1つのSQLで“失敗行”を明示的に返す個別検証です。
この記事では、Analytics Engineer試験の出題観点にも触れつつ、singular testの設計・実装・運用の勘所を短時間で押さえます。
dbtのsingular testは、tests/配下に置く1つのSQLファイルです。そのSQLが“失敗している行”を返すように書くのがルールで、実行時は0行でパス、1行以上で失敗となります。複雑な結合や条件、時間的整合性など、generic testで表現しづらい要件に最適です。
generic testは再利用可能なマクロベースの検証で、not_nullやuniqueやrelationshipsなどが代表例です。singularは使い捨ての個別ロジックに向き、where句やウィンドウ関数、結合など自由度が高いのが特徴です。
| 項目 | Generic test | Singular test | DBネイティブ制約 |
|---|---|---|---|
| 定義方法 | マクロ+YAML引数で再利用 | 個別SQLで失敗行を返す | DDLで制約(UNIQUE/NOT NULLなど) |
| 表現力 | 共通パターンに強い | 任意のSQL(結合・ウィンドウ関数等) | 制約種別に限定 |
| 再利用性 | 高い(同じ型に適用) | 低い(ケースバイケース) | 中(テーブル単位で汎用) |
| クロスモデル検証 | 限定的(relationships等) | 自由(複数モデルを結合可) | 基本不可(単一テーブル中心) |
| 実行と可視化 | dbt testで集計実行 | dbt testでそのまま実行 | DWHが自動で検知・拒否 |
| ユースケース | NOT NULL/UNIQUE/関係整合 | 時間重複・条件付き一意・不変条件逸脱 | キー一意や外部キー(対応DBのみ) |
最小のsingular test例(負の金額が存在しないことを検証)
{{ config(severity='error', tags=['dq','critical']) }}
-- tests/no_negative_amounts.sql
with invalid as (
select order_id, amount
from {{ ref('orders') }}
where amount < 0
)
select * from invalidsingular testはtests/配下に配置します。命名は検証対象と違反内容がわかるように、<対象>__<ルール>.sql のようなスキームが現場で読みやすく、試験でも“tests/配下の*.sqlがテストになる”という理解が重要です。
Jinjaを使ってref/sourceや変数、マクロを呼び出せます。重複するロジックはmacros/に切り出してテストSQLから呼ぶと保守が楽です。
構成例(テストとマクロ)
# プロジェクト構成(抜粋)
models/
mart/
orders.sql
macros/
test_helpers.sql
tests/
orders__no_negative_amounts.sql
-- macros/test_helpers.sql
{% macro flag_negative_amounts(model_name) %}
select order_id, amount
from {{ ref(model_name) }}
where amount < 0
{% endmacro %}
-- tests/orders__no_negative_amounts.sql
{{ config(severity='error', tags=['dq']) }}
{{ flag_negative_amounts('orders') }}条件付き一意性(例: status='active' の行だけ顧客メールが一意)や、期間の重複(SCD2/有効期間のオーバーラップ)、クロスモデルの業務ルール(注文が出荷後にキャンセルされていないか)などは、singular testが得意です。
以下は、有効期間の重複を検出するsingular testの例です。行が返れば重複=失敗です。
期間オーバーラップ検出(SCD2の有効期間が重複しないこと)
{{ config(severity='error', tags=['dq','temporal']) }}
with scoped as (
select surrogate_key, valid_from, valid_to
from {{ ref('dim_customer_scd2') }}
),
ordered as (
select
surrogate_key,
valid_from,
coalesce(valid_to, '9999-12-31') as valid_to
from scoped
),
overlaps as (
select a.surrogate_key, a.valid_from as a_from, a.valid_to as a_to,
b.valid_from as b_from, b.valid_to as b_to
from ordered a
join ordered b
on a.surrogate_key = b.surrogate_key
and a.valid_from < b.valid_to
and b.valid_from < a.valid_to
and (a.valid_from, a.valid_to) <> (b.valid_from, b.valid_to)
)
select * from overlaps基本の実行は dbt test です。個別に実行するにはセレクタを使います。特にsingular testはファイルパスやタグでの選択が実務的で確実です。失敗行の永続化が必要なら、テストに store_failures: true を設定します。
セレクタ例: path:tests/orders__no_negative_amounts.sql や tag:dq など。CIでは変更影響のあるテストだけを実行するために --select state:modified+ とpathの併用が有効です。
dbt test 実行フロー(singular)
CLI(db t test)
|
v
Compile Jinja -> SQL
|
v
Run query (returns failing rows)
|
|-- zero rows --> PASS
|
`-- >0 rows --> FAIL
|
`-- store_failures=true ? persist failing rows : no persistCLIとconfig例(タグ選択・警告扱い・失敗行の保存)
# 個別ファイルを実行
$ dbt test -s path:tests/orders__no_negative_amounts.sql
# タグで実行
$ dbt test -s tag:dq
# テストファイル先頭の設定例
{{ config(
tags=['dq','nightly'],
severity='warn',
store_failures=true
) }}singular testは自由度が高い分、重いSQLになりがちです。対象を最小限に絞り、必要な列のみを返し、結合条件とインデックス(クラスタリング/ソートキー)を意識します。重複検出はまず集約で候補キーを絞り、サンプルのみ詳細列を返すと軽くなります。
テストは決定的であるべきです。現在時刻やランダム値に依存させず、環境差に敏感な関数・ヒントは極力避けます。
重複検出を軽くするパターン(候補を先に絞る)
{{ config(severity='error', tags=['dq','perf']) }}
with active as (
select customer_id, email
from {{ ref('dim_customer') }}
where status = 'active'
),
candidates as (
select email
from active
group by email
having count(*) > 1
),
details as (
select a.customer_id, a.email
from active a
join candidates c using(email)
)
select * from detailssingular testは“失敗行を返すSQL”であること、tests/配下の*.sqlがテストになること、configでseverityやtags、store_failuresを設定できることは頻出ポイントです。genericとの使い分けを言語化できると有利です。
セレクタは path: と tag: を押さえ、ピンポイント実行のオプションを選べるようにしておきましょう。
条件付き一意性(activeユーザーのメールは一意)
{{ config(severity='error', tags=['dq','exam']) }}
with active as (
select user_id, email
from {{ ref('users') }}
where is_active = true
),
dups as (
select email
from active
group by email
having count(*) > 1
)
select a.*
from active a
join dups d using(email)Analytics Engineer
問題 1
dbtのsingular testに関する説明として最も適切なものはどれか?
正解: A
singular testは個別SQLで失敗行を返す設計で、0行ならパス、1行以上で失敗。count(*)の返却やYAMLのみの定義は要件ではなく、{{ ref() }}や{{ source() }}は通常どおり使用できる。
store_failures=trueにすると何が起きますか?
テストで検出された“失敗行”がデータウェアハウス上のテーブル/ビューとして永続化され、後続の調査やダッシュボードでの可視化に利用できます。命名や格納先はdbtの既定に従います。
generic testで足りない場合に、まず検討すべきことは?
要件を分解して既存のgeneric test(unique, not_null, relationships など)とフィルタの組み合わせで代替できないかを確認します。それでも表現困難な複合条件・時間整合・クロスモデル検証はsingular testで実装します。
ephemeralモデルやseedをsingular testから参照できますか?
はい。{{ ref() }}や{{ source() }}は通常どおり使用でき、ephemeralはサブクエリとしてインライン展開されます。seedは物理テーブル/ビューとして参照されます。
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)、設定優先度...