青の統計学-DS Playground-

singular tests:ビジネスルールを SQL で断言

Stage 2 — 第4章 | dbt入門カリキュラム 推定学習時間:35〜45分 | 難易度:★★★★☆


この章で学ぶこと

singular testtests/ 配下の SQL ファイルです。 dbt は build 時にこの SQL を実行し、結果 0 行 → 成功1 行でも返る → 失敗 という単純な契約で動きます。

前章 の schema test が「列単位の宣言的チェック」なら、singular test は schema test では書きにくいビジネスルール を SQL で断言する層です。 範囲チェック、grain の一意性、seed との被覆——手続き的・横断的 な条件はこちらに回します。

まず dbt の作法を整理し、続けて DS Playground の assert_skill_check_total_score_valid を中心に、プロジェクト内 singular test 群の 読み方・設計意図 を学びます。

この章を終えると、こんなことができるようになります:

  • singular test SQL(「失敗行 SELECT」)の 構造 を読める
  • スコア範囲・カタログ被覆・latest-state 一意性など ルールの種類 を分類できる
  • _tests.yml の description と SQL を セットで 解釈できる
  • 新ルール追加時に schema vs singular の 選択基準 を説明できる

singular test の基本形

-- tests/assert_skill_check_total_score_valid.sql
          select
            *
          from {{ ref('stg_supabase__skill_check_results') }}
          where total_score < 0
            or total_score > 100
          
要素 意味
ref('stg_...') テスト対象モデル(staging でも mart でも可)
WHERE 許されない行 の条件
SELECT * 失敗時に 証拠行 をログへ
flowchart LR SQL["tests/*.sql
失敗行を SELECT"] BUILD["dbt build"] OK["0 行 → 成功"] NG["1 行以上 → CI 失敗"] SQL --> BUILD BUILD --> OK BUILD --> NG

データ品質チェック で手書きする WHERE score > 100常時 CI に載ったイメージです。 singular test は「難しい SQL」ではなく、組織が守りたい断言の一覧 です。


schema test との使い分け

条件 推奨
単列 NOT NULL / enum schema test
数値範囲(不等号) singular
複数列 grain singular の GROUP BY ... HAVING
複数モデル JOIN singular
版依存 JSON singular + 詳細 description
宣言的・列単位 → schema(YAML)
          手続き的・横断   → singular(SQL)
          

accepted_values離散値のリスト 向けです。 total_score < 0 OR total_score > 100 のような 連続値の範囲 は singular の方が読みやすく、description も書きやすいです。


assert_skill_check_total_score_valid

_tests.yml の説明(要約):

スキルチェック総合スコアが 0〜100 の範囲に収まることを確認する。 日次平均スコア mart で異常値がそのまま BI に出ることを防ぐ。

flowchart LR STG[stg_supabase__skill_check_results] TST[assert_skill_check_total_score_valid] MART[fct_skill_check_daily] STG --> TST STG --> MART TST -.->|失敗で CI 停止| MART
観点 内容
対象 staging(問題の早い段階で検知)
ルール 総合点 0〜100(製品仕様)
下流影響 日次平均など 集計が異常値を増幅 する前に止める

4 行の SQL ですが、BI 上の平均スコアを 説明可能な域 に留めるための最小ガードです。 staging で止める理由は、汚染データが intermediate / mart へ伝播する前 に失敗させるためです。


プロジェクト内の singular test 一覧

DS Playground の analytics_mcp プロジェクトには、用途別に複数の singular test があります。

テスト名 断言内容 種類
assert_skill_check_total_score_valid total_score ∈ [0, 100] 範囲
assert_skill_check_axis_scores_valid 版ごとの JSONB 軸スコアが存在し 0〜100 版依存
assert_skill_check_version_supported normalized_version がサポート版のみ enum 拡張
assert_exam_scores_valid 模擬試験点が 0〜満点 範囲
assert_problem_attempts_unique_current_state 最新状態表の grain 一意 grain
assert_user_curriculum_page_progress_unique 1 user × 1 page grain
assert_problem_attempts_catalog_covered 回答 problem が seed カタログに存在 被覆
assert_practice_problem_catalog_unique seed 側の grain 一意 grain
assert_member_survey_selected_options_non_empty アンケート選択肢 1 個以上 複合
assert_user_engagement_usage_intensity_score_valid エンゲージメント濃度 0〜100 範囲

一覧を眺めると、範囲・grain・被覆・版依存 の 4 カテゴリに分類できます。 新ルール追加時は「どのカテゴリか」を先に決めると、schema に回すべきか singular に回すべきかが見えやすくなります。


版依存ルール — 軸スコアの例

assert_skill_check_axis_scores_validgenege-v3(6 軸)data-talent-v1(4 軸) で JSON 構造が異なります。 列単位の accepted_values ではなく SQL + 条件分岐 が必要です。

-- 概念例(簡略)。実ファイルは版ごとに CASE で検証
          select *
          from {{ ref('stg_supabase__skill_check_results') }}
          where
            (normalized_version = 'genege-v3'
              and (legacy_statistics_score < 0 or legacy_statistics_score > 100))
            or
            (normalized_version = 'data-talent-v1'
              and (data_science_score < 0 or data_science_score > 100))
          -- ... 他軸も同様
          

第2章 で学んだ normalized_version とセットです。 staging で 版の意味を明示 し、singular で 版ごとの列セット を検証する——二段構えで semantics を守ります。


grain 断言 — latest state 表

problem_attempts再回答で UPDATE される最新状態 です。 sources YAML の description にも latest state であることが書かれており、singular test が ドキュメントと検証を一致 させています。

同一 user × category × section × problem に複数行あると、正答率 mart が 二重計上 されます。

-- assert_problem_attempts_unique_current_state(概念)
          select
            user_id,
            category_slug,
            section_slug,
            problem_id,
            count(*) as row_count
          from {{ ref('stg_supabase__problem_attempts') }}
          group by 1, 2, 3, 4
          having count(*) > 1
          

メタデータと来歴 で学んだ「ドキュメントと検証の一致」が、ここで具体化されています。 第1章 で latest-state 表の読み方を学んだなら、この test は その grain 契約の機械的保証 です。


カタログ被覆 — seed との関係

アプリ DB だけでは「存在すべき全問題」が見えません。 seed practice_problem_catalog との整合を singular test で守ります。

-- assert_problem_attempts_catalog_covered(概念)
          select distinct
            pa.category_slug,
            pa.section_slug,
            pa.problem_id
          from {{ ref('stg_supabase__problem_attempts') }} pa
          left join {{ ref('practice_problem_catalog') }} cat
            on pa.category_slug = cat.category_slug
           and pa.section_slug = cat.section_slug
           and pa.problem_id = cat.problem_id
          where cat.problem_id is null
          
失敗時の意味 対応
アプリに存在する problem_id が seed にない slug 変更・seed 未更新
mart の表示名欠落 fct_problem_quality のレビュー不可
flowchart LR ATT["stg_supabase__problem_attempts"] SEED["practice_problem_catalog"] TST["assert_problem_attempts_catalog_covered"] MART["fct_problem_quality"] ATT --> TST SEED --> TST ATT --> MART SEED --> MART

次章 で seed の設計意図を詳述します。


失敗時の読み方(運用)

singular test が CI で落ちたときの読み方は次の 3 ステップです。

  1. 失敗 SQL が どの mart 意思決定を守るか_tests.yml で読む
  2. 返却行の列が 修正すべきソース / staging / seed を示す
  3. 一時的に test を消さず、定義かソースか seed を直す

品質インシデント の「テストを無効化して黙らせる」アンチパターンと同型です。 singular test の失敗行は、修正すべき場所へのポインタ として読むのが現場の作法です。


体系コラム:カリキュラム上の位置づけ

項目 内容
Stage / 章 Stage 2 — 第4章
今回の論点 複雑な業務ルールを 失敗行 SQL で固定
前章との接続 schema tests
次章への伏線 seeds カタログ

まとめ

テスト種 代表例 守るもの
範囲 assert_skill_check_total_score_valid 0〜100 スコア
grain assert_problem_attempts_unique_current_state latest state 一意
被覆 assert_problem_attempts_catalog_covered seed との整合
assert_skill_check_version_supported サポート版のみ

schema test が 列契約の宣言 なら、singular test は 横断ルールの断言 です。 両方そろって初めて、パイプライン全体の品質ゲートが機能します。


関連教材


確認問題

問題 1

singular test が返すべき行数とテスト結果の関係として正しいのはどれですか。

A. 1 行以上で成功
B. 0 行で成功、1 行以上で失敗
C. 常に失敗
D. 行数は関係ない

正解: B


問題 2

数値列の 0〜100 範囲チェックを singular test にした 最も適切な理由 はどれですか。

A. ref() が singular でしか使えない
B. 連続値の範囲条件は accepted_values では表しにくい
C. staging には schema test を書けない
D. YAML ファイルが 1MB 制限がある

正解: B


問題 3

fact 表の外部キーが seed マスタに存在するかを singular test で検証する 主な目的 はどれですか。

A. ユーザーの退会を検知する
B. 参照整合性の崩れ(未知 ID の混入)を CI で早期検知する
C. BI ツールのキャッシュ期限切れを防ぐ
D. dbt の profile 名の typo を直す

正解: B


用語メモ(この章)

用語 意味(この章での使い方)
singular test tests/ 配下の任意 SQL テスト
失敗行 条件を満たす=許されない行
範囲断言 < 0 OR > 100
grain 断言 GROUP BY + HAVING count > 1
被覆(coverage) 参照整合が取れているか