青の統計学-DS Playground-

staging:ソース意味を残す薄い変換

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


この章で学ぶこと

前章 では、source()プロジェクト外の表 を宣言する作法を学びました。 staging は、その直後のレイヤーです。1 つの source 表に対して 1 つの staging モデル を置き、列名・型・日付・軽い行フィルタだけを整えます。

ここでいう「正規化」は、データベース理論の第 3 正規形(3NF)とは別物です。 dbt コミュニティでは Thin staging(薄い staging)と呼ばれることが多く、ソースの意味を消さずに分析向けに読みやすくする 層だと捉えてください。

まず staging で何を書き、何を書かないかを整理し、続けて DS Playground の stg_supabase__* を読み解きます。

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

  • staging と intermediate / marts の 責務の境界 を説明できる
  • staging SQL の列リネーム・WHERE 句を 意図として 読める
  • latest-state 表と event-history 表で staging の読み方が 変わる理由 を説明できる
  • staging に書いてはいけない処理(JOIN・集計・KPI 定義)を見分けられる

パイプラインの中で staging はどこか

Transform 全体は、だいたい次の順序で積み上がります。

flowchart LR SRC["source()
アプリ DB"] STG["staging
薄い整形"] INT["intermediate
定義の固定"] MART["marts
BI 向け完成表"] SRC --> STG --> INT --> MART

staging の入力は source() だけ が理想です。 下流の intermediate は ref('stg_...') で staging を読み、複数 source の統合や KPI の定義 を担います。

もし staging を飛ばして marts から source() を直叩きすると、同じ生表への依存が mart ごとに散らばります。 もし staging で JOIN や GROUP BY までやってしまうと、「この KPI の正本はどのモデルか」 が intermediate と競合し、SSOT が分裂します。

staging は 速く・薄く・1 表 1 モデル — この 3 点を守る層です。


staging でやること・やらないこと

やること

staging では、だいたい次のような処理に留めます。

列名の統一 … アプリ都合の短い名前(id)を、下流で grain が分かる名前(consent_id)に変える。
型と日付の明示 … タイムスタンプ列に加え、日次集計用の *_date 列を派生させる(次章 で詳述)。
軽い行フィルタ … 分析 grain が成立しない行(キー NULL など)を除外する。
ソース列の意味を残すリネームsource のような紛らわしい列名を consent_source などに変える。
不要列の早期ドロップ … 下流に渡す必要のない PII 列を、ここで意図的に出さない。

これらはすべて 「ソースを読みやすくする」 ための処理で、ビジネス判断(Premium か Free か、アクティブかどうか)ではありません。

やらないこと

次は intermediate 以降 の仕事です。

  • 複数表の JOIN で母集団を定義する
  • GROUP BY で KPI を計算する
  • セグメント(プラン種別・会員状態)を付与する
  • Metabase 向けの最終粒度(1 日 1 行など)に畳む

データアナリティクス入門 で 1 本の SQL に書いていた JOIN + 集計は、dbt では 層に分けて 書きます。 staging はその 最初の 1 層 です。


DS Playground の staging レイヤ

DS Playground の dbt プロジェクトでは、staging モデルは models/staging/supabase/ に置かれ、命名は stg_supabase__{source表名} です。

規則
接頭辞 stg_ staging 層であること
ソース名 supabase__ source('supabase', ...) 由来
サフィックス 生表名と 1 対 1(consentsstg_supabase__consents

dbt_project.yml では staging は view が既定です。ソースに近い薄い層は view にし、重い集計は marts の table に寄せる——この分担は 第3章 で触れました。

flowchart TB subgraph raw["Supabase public(source)"] C[consents] P[problem_attempts] E[exam_attempt_events] end subgraph stg["staging views"] SC[stg_supabase__consents] SP[stg_supabase__problem_attempts] SE[stg_supabase__exam_attempt_events] end C --> SC P --> SP E --> SE

1 生表 1 staging にしておくと、ソース側の列変更が どの staging に効くか が一目で分かります。 Lineage 上も、consentsstg_supabase__consents → … と辿りやすくなります。


stg_supabase__consents

consents は、利用規約・プライバシー・マーケティング等の 同意履歴 を append-only で保持する source 表です。 DS Playground では terms 同意済み user_id を会員数 proxy の母集団として使う、という設計判断が source YAML の description にも書かれています。

staging モデルは次のとおりです。

select
            id as consent_id,
            user_id,
            consent_type,
            consent_version,
            is_granted,
            captured_at,
            cast(captured_at as date) as captured_date,
            source as consent_source,
            meta
          from {{ source('supabase', 'consents') }}
          where user_id is not null
            and captured_at is not null
          

列リネームの意図

id as consent_id … 下流で「1 行 = 1 同意レコード」の grain が分かるよう、汎用名 id を具体名に変えています。
user_id … そのまま。会員 proxy 集計の結合キーになります。
consent_type … terms / privacy / marketing / job_opt_in など、同意の種類 を残します。staging で種類を絞らない——「terms だけ」などのビジネスフィルタは intermediate / mart 側の責務です。
captured_at / captured_date … イベント時刻と日次集計用の日付列。日付列の命名規約は次章で横断的に扱います。
source as consent_source … 生表の列名 source(保存元を示すアプリ列)は、dbt の source() マクロや SQL の文脈と 紛らわしい ためリネームしています。
meta … JSON の補足。必要になったら下流で展開しますが、staging 段階では 意味を消さずに残す 判断です。

WHERE 句の意図

where user_id is not null
            and captured_at is not null
          

同意の主体(誰の同意か)と時刻(いつの同意か)が NULL の行は、分析 grain として成立しません。 日次 mart や会員 proxy の下流で 「誰の・いつの」 が必須になるため、staging で早めに落とします。

これは schema test(第3章)の前段でもあります。 not_null テストが失敗する前に、明らかに使えない行を除外しておく、という 二重の安全网 です。

意図的に出力しない列

_staging__models.yml には、consents から ip_address や user_agent を mart に出さない 旨が書かれています。 staging SQL に列自体が現れない、というのが 最小列・PII 拡散防止 の実装です。

分析に不要な個人関連列を staging で止めておけば、下流の intermediate / marts / BI 利用者が うっかり SELECT * しても拡散しにくくなります。 データマネジメント入門の最小権限・利用 と同じ発想です。


latest-state と event-history — staging の読み方が変わる点

source の 性質 によって、staging で意識すべき点が少し変わります(第4章 で触れた区別の続きです)。

latest-state 表:problem_attempts

練習問題の 最新保存状態 を 1 行で保持する表です。同じユーザー・問題を再回答すると 行が UPDATE されます。 staging では「履歴が消える」ことを YAML / description で明示し、正答率は最新状態精度 であることを下流に伝えます。

-- 構造のイメージ(抜粋)
          select
            id as problem_attempt_id,
            user_id,
            problem_id,
            attempted_at,
            cast(attempted_at as date) as attempted_date,
            is_correct,
            source as latest_attempt_source
          from {{ source('supabase', 'problem_attempts') }}
          where user_id is not null
          

staging では 再回答で上書きされる というソースの性質を変えません。 「初回正解率」を取りたければ intermediate で別ロジックが必要——それを staging でごまかさない、のが Thin staging です。

event-history 表:exam_attempt_events

模擬試験の 保存イベント は 1 回 1 行で append されます。 同じユーザーが複数回保存すれば 複数行 残り、アクティビティ指標(「明示的な学習行動があったか」)の material になります。

staging では行を集約せず、イベント 1 件 1 行 のまま整えます。 複数 source のイベントを UNION ALL して Active User の SSOT を作るのは、intermediate の int_user_activity_events の仕事です(Stage 3)。

性質 staging でやること staging でやらないこと
latest-state 上書きされる意味を残す 履歴復元・初回抽出
event-history 1 イベント 1 行を維持 日次 DAU の COUNT

staging に書いてはいけない例

次は アンチパターン です。staging で「アクティブユーザー」を定義しようとしています。

-- アンチパターン: staging で指標化
          select
            user_id,
            count(*) as activity_count,
            case when count(*) > 0 then true else false end as is_active_today
          from {{ ref('stg_supabase__problem_attempts') }}
          group by 1
          

問題は 3 つあります。

  1. ソースが 1 つだけ … DS Playground のアクティブ定義は、問題保存・模試保存・カリキュラム完了など 複数 source の統合です。
  2. SSOT 分裂 … 正本は intermediate の int_user_activity_events に置いているのに、別定義が生えます。
  3. 再利用性 … mart ごとに似た GROUP BY が増え、DM 入門で学んだ 野良マート に近づきます。

正しい配置は、staging で各 source を薄く整え、intermediate で イベント型を揃えて UNION し、mart で 日次 KPI を出す、という順序です。


view として build する理由

staging を view にするのは、次のトレードオフからです。

メリット: Supabase public の変更が、次回 dbt build で staging に反映されやすい。ストレージを増やさず、ソース近傍の定義を保てる。
デメリット: 非常に重い変換を staging に載せると、下流が参照するたびに再計算コストがかかる。

そのため DS Playground では 重い集計は marts(table) に寄せ、Metabase には marts を主に公開します。 staging は 開発者・dbt docs を読む人 がソース semantics を理解する層、という使い分けです。


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

項目 内容
Stage / 章 Stage 2 — 第1章
今回の論点 Thin staging — ソース semantics を残す薄い変換
前章との接続 source 定義
次章への伏線 派生列と命名*_date 規約

まとめ

staging は「とにかくきれいにする」層ではなく、ソースを分析向けに読みやすくし、危ない列や grain 外の行を早めに止める 層です。 DS Playground の stg_supabase__consents をはじめ、各 stg_supabase__*1 生表 1 モデル の規律のもと、intermediate への入力を整えています。


関連教材


確認問題

問題 1

staging 層の責務として 最も適切 なのはどれですか。

A. DAU / WAU / MAU の最終計算
B. 1 生表の列リネーム・軽い型変換・明らかな NULL 行除外
C. BI ダッシュボードのレイアウト設計
D. Stripe Webhook の受信

正解: B

解説: 指標化・統合は intermediate / marts です。staging は Thin staging に留めるのが一般的です。


問題 2

生表の列 sourceorder_source にリネームする理由として 最も近い のはどれですか。

A. 英語表記に統一するため
B. dbt の source() や SQL 文脈と、生表の列名 source の混乱を避けるため
C. PostgreSQL の予約語を避けるため(source は予約語ではない)
D. seed ファイルとの結合のため

正解: B


問題 3

append-only のイベント履歴表を staging する際、避けるべき 処理はどれですか。

A. タイムスタンプ列を残す
B. cast(timestamp as date) で日付列を派生する
C. 同一ユーザーの複数イベントを GROUP BY で 1 行に畳む
D. user_id is not null で grain 外の行を除外する

正解: C

解説: イベント履歴は 1 イベント 1 行 を staging で維持します。集約は intermediate / mart の責務です。


用語メモ(この章)

用語 意味(この章での使い方)
Thin staging ビジネスロジックを載せない薄い staging
grain 1 行が表す事象の粒度(例: 1 同意 1 行)
latest-state 再回答・更新で行が上書きされる表
event-history イベントごとに行が append される表
view staging の既定 materialization