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 全体は、だいたい次の順序で積み上がります。
アプリ 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(consents → stg_supabase__consents) |
dbt_project.yml では staging は view が既定です。ソースに近い薄い層は view にし、重い集計は marts の table に寄せる——この分担は 第3章 で触れました。
1 生表 1 staging にしておくと、ソース側の列変更が どの staging に効くか が一目で分かります。
Lineage 上も、consents → stg_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 つだけ … DS Playground のアクティブ定義は、問題保存・模試保存・カリキュラム完了など 複数 source の統合です。
- SSOT 分裂 … 正本は intermediate の
int_user_activity_eventsに置いているのに、別定義が生えます。 - 再利用性 … 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
生表の列 source を order_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 |