イベント統合:Active User の SSOT を intermediate に作る
Stage 3 — 第1章 | dbt入門カリキュラム 推定学習時間:35〜45分 | 難易度:★★★☆☆
この章で学ぶこと
Stage 2 までで、1 source 表 → 1 staging モデルの 薄い整形 を学びました。 次は intermediate(中間)層 です。ここでは複数 staging を横断し、「この指標は何を数えるか」 を 1 箇所に固定します。
KPI が 1 つのテーブルに収まらないとき(例: アクティブユーザー、ファネル、複数チャネルのイベント統合)、mart ごとに同じ UNION や JOIN を書くと定義が散らばります。 intermediate は、その SSOT(Single Source of Truth) を担う層です。
まず dbt の作法を整理し、続けて DS Playground の int_user_activity_events を読み解きます。
この章を終えると、こんなことができるようになります:
- intermediate 層の 責務 を staging / marts と区別して説明できる
UNION ALLで複数 staging を統合する 理由と限界 を説明できる- 共通列(
occurred_at,date_day,event_type)に揃える設計を読める - 中間モデルが複数 mart の 共通入力 になる構造を mermaid で示せる
パイプラインの中で intermediate はどこか
データマネジメント入門の Medallion 三層 では、Staging → Intermediate → Mart の流れを学びました。
1 表 1 モデル"] INT["intermediate
定義の固定"] MART["marts
BI 向け集計"] STG --> INT --> MART
| 層 | 入力 | 典型処理 | 典型出力 |
|---|---|---|---|
| staging | source() |
列リネーム、日付派生、軽いフィルタ | 1 source 表に対応する view |
| intermediate | ref('stg_...') |
複数 staging の UNION、共通語彙、定義の固定 | 指標 SSOT(イベント行など) |
| marts | ref('int_...') |
GROUP BY、セグメント分割、BI grain | ダッシュボード向け完成表 |
intermediate の入力は staging だけ が理想です。
mart から source() を直叩きしたり、staging で済ませきれない KPI 定義 を mart ごとにコピーすると、ワイルドマートと SSOT で学んだ「属人 SQL の乱立」と同型の問題が dbt でも起きます。
intermediate でやること・やらないこと
やること
- 複数 staging の統合 …
UNION ALLで縦に積み、共通列名に揃える - 指標定義の固定 … 「アクティブとは何か」「コンバージョンイベントはどれか」を 1 モデルに書く
- 共通語彙の付与 …
event_type,occurred_at,date_dayなど横断列 - 最小限の行フィルタ … grain が成立しない行(キー NULL 等)の除外
- schema test による契約 … Stage 2 第3章 で学んだ enum / not_null
やらないこと
- 日次 KPI の最終集計(
GROUP BY date_dayで DAU を出す)→ marts - セグメント別の内訳(プラン種別、地域別)→ 別 mart または下流
- BI 向けの列選択・リネーム → marts
- date spine によるゼロ埋め → marts(必要なら)
intermediate は 「何を数えるか」の正本、mart は 「どう見せるか(grain・セグメント)」 — この分離が dbt 設計の核心です。
UNION ALL によるイベント統合
複数ソースから「ユーザーが何かをした」行を 1 つの SSOT にまとめる典型パターンが UNION ALL です。
-- 概念形
select user_id, order_placed_at as occurred_at, order_placed_date as date_day, 'order_placed' as event_type
from {{ ref('stg_app__orders') }}
union all
select user_id, page_viewed_at as occurred_at, page_viewed_date as date_day, 'page_viewed' as event_type
from {{ ref('stg_app__page_views') }}
設計上のポイントは 3 つです。
UNIONではなくUNION ALL… 重複排除は意図的に行いません。同一ユーザーが同日に複数イベントを起こせば複数行になります。DAU はcount(distinct user_id)で数えるため、行の重複は問題になりません。列名の揃え方 … ソースごとにタイムスタンプ列名は異なります(
attempted_at,completed_atなど)。staging で*_dateを派生し、intermediate ではoccurred_at/date_dayに統一 します(Stage 2 第2章)。フィルタは最小限 … 「会員だけ」「有料だけ」などのビジネス分割は mart 側 に寄せ、SSOT は観測された行動を忠実に残します。
UNION ALL + 共通列"] M1["mart: 日次 DAU"] M2["mart: セグメント別"] M3["mart: イベント種別"] S1 --> INT S2 --> INT S3 --> INT INT --> M1 INT --> M2 INT --> M3
int_user_activity_events
DS Playground では、アクティブユーザー指標の SSOT として int_user_activity_events が置かれています。
6 種類の staging から学習行動イベントを UNION ALL で縦に積み、共通 4 列に揃えます。
Learning Action SSOT"] M1["fct_active_users_daily"] M2["fct_active_users_daily_by_member_status"] M3["fct_activity_events_daily_by_type"] M4["fct_user_engagement_current"] PA --> INT EX --> INT CP --> INT SC --> INT LP --> INT MS --> INT INT --> M1 INT --> M2 INT --> M3 INT --> M4
出力 grain: 1 行 = 1 ユーザーの 1 学習行動イベント。
| カラム | 意味 |
|---|---|
user_id |
ユーザー ID(mart では原則非公開) |
occurred_at |
行動が記録されたタイムスタンプ |
date_day |
日次集計用の日付(Stage 2 の横断名) |
activity_type |
行動種別(後述の 6 値) |
YAML description には、Active User の定義が明示されています。
A user is active in a period when the user has at least one row in this model during that period.
SQL の読み方(概念)
with activity_events as (
select
user_id,
attempted_at as occurred_at,
attempted_date as date_day,
'practice_problem_saved' as activity_type
from {{ ref('stg_supabase__problem_attempts') }}
union all
select
user_id,
recorded_at as occurred_at,
recorded_date as date_day,
'mock_exam_saved' as activity_type
from {{ ref('stg_supabase__exam_attempt_events') }}
-- curriculum / skill_check / learning_goal / survey も同様
)
select
user_id,
occurred_at,
date_day,
activity_type
from activity_events
where user_id is not null
and occurred_at is not null
and date_day is not null
カリキュラム進捗は status = 'completed' and completed_at is not null のように、ソースごとに最小限のフィルタ だけが intermediate に入ります。
「terms 会員だけ」などの分割は mart 側の責務です(第4章)。
activity_type 語彙(6 種類)
DS Playground では、明示的な保存・完了・送信 だけを Learning Action として数えます。 パッシブなセッションやページビューは Active User に含めません。「ログインしただけ」では DAU は増えず、何かを保存・完了・送信したとき初めて 1 行が SSOT に載ります。 これは 指標定義 で学ぶ「何を数えるか」の具体例です。
activity_type |
由来 staging | 意味 |
|---|---|---|
practice_problem_saved |
problem_attempts | 演習問題の最新状態を保存 |
mock_exam_saved |
exam_attempt_events | 模擬試験結果を保存 |
curriculum_page_completed |
user_curriculum_page_progress | カリキュラムページ完了 |
skill_check_completed |
skill_check_results | スキルチェック完了 |
learning_goal_updated |
user_learning_preferences | 学習目的の更新 |
member_survey_submitted |
member_survey_responses | アンケート送信 |
activity_type には schema test の accepted_values が付き、新しい行動を追加するときは SQL・YAML・docs をセットで更新 します。
SSOT としての責務境界
| intermediate が担う | mart が担う |
|---|---|
| 行動定義の統一 | 日次・ステータス別・タイプ別の集計 |
| 必須列の NULL 除外 | 前日比・構成比・エンゲージメント tier |
| 複数 staging の結合 | date spine によるゼロ埋め |
int_user_activity_events を変更すると、依存する すべての activity mart の数字が変わります。
だからこそ メタデータとリネージ で学んだ lineage 記録が重要です。
よくある誤解
| 誤解 | 実際 |
|---|---|
| 「会員登録者だけ SSOT に入れるべき」 | ベース activity mart は 観測行動を忠実に 数える。会員分割は別 mart |
| 「UNION ALL だから DAU が水増しする」 | DAU は distinct user_id で数える |
| 「intermediate は便利な CTE」 | 組織が合意した定義の正本。mart の共通入力 |
体系コラム:カリキュラム上の位置づけ
| 項目 | 内容 |
|---|---|
| Stage / 章 | Stage 3 — 第1章 |
| 今回の論点 | UNION ALL によるイベント SSOT と mart 共通入力 |
| 前章との接続 | seeds カタログ |
| 次章への伏線 | mart 設計 |
まとめ
複数 staging を mart ごとに結合すると KPI 定義が散らばります。
intermediate で UNION ALL + 共通列 + 最小フィルタにより 定義を 1 箇所に固定 し、下流 mart がその SSOT を共有する——これが Stage 3 の出発点です。
DS Playground では int_user_activity_events が DAU・エンゲージメント・incremental mart の共通入力になっています。
関連教材
- 前章: seeds カタログ
- 次章: mart 設計
- 関連: Medallion 三層
確認問題
問題 1
複数 staging を UNION ALL で統合する intermediate モデルの 主な目的 として最も適切なのはどれですか。
A. 重複行を自動的に削除するため
B. 各ソースの行を縦に積み、共通のイベント定義 SSOT を作るため
C. SQL の実行速度を必ず向上させるため
D. source() を使わずに済ませるため
正解: B
解説: UNION ALL は重複排除をしません。目的は 定義の一元化 です。重複排除や集計は下流の mart で行います。
問題 2
intermediate 層に 置くべきでない 処理として最も適切なのはどれですか。
A. 複数 staging の UNION ALL
B. 共通列名へのリネーム(例: occurred_at, date_day)
C. 日次 KPI の最終集計(GROUP BY date_day)
D. イベント種別列の付与(例: event_type)
正解: C
解説: 集計と BI 向け粒度の確定は marts の責務です。intermediate は 定義の固定 に留めます。
問題 3
ベースの指標 mart と、セグメント別 mart を 分ける 主な理由として最も本質的なのはどれですか。
A. SQL が長いから
B. grain と business question が異なるから
C. YAML が書けないから
D. incremental だから
正解: B
解説: 全体の faithful count と、セグメント別の内訳は 別の問い です。1 表に混ぜると grain が壊れやすくなります。
用語メモ(この章)
| 用語 | 意味(この章での使い方) |
|---|---|
| intermediate | staging と marts の間。定義の固定 |
| SSOT | 指標定義の単一正本 |
UNION ALL |
重複排除なしの縦結合 |
| Learning Action | DS Playground で明示的に数える学習行動 |
| faithful count | 観測データをセグメント都合で落とさない数え方 |