DAU移動平均 — ウィンドウフレーム ROWS BETWEEN で N日移動平均を計算する
日次 DAU はノイズ(曜日効果・キャンペーン単発)が大きく、生の折れ線ではトレンドが読み取れません。そこで N日移動平均(Moving Average) で平滑化します。LAG では「1点」しか取れませんが、ウィンドウフレーム を使うと「現在行を含む直近N行の集合」を対象に集計できます。
AVG(dau) OVER ( ORDER BY metric_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) -- 「現在行」と「その2行前まで」=計3行を平均 → 3日移動平均 -- 先頭付近は枠内の行数が3未満でも、存在する行だけで平均する
ROWS は物理的な行数で枠を決めます。RANGE は ORDER BY 値が同じ行(ピア)をまとめて扱うため、日付に重複があると意図せず多くの行が枠に入ります。移動平均は行数で数える ROWS を使うのが定石です。daily_active テーブルから、各日の DAU と「3日移動平均(dau_ma3)」を計算してください。出力列は metric_date, dau, dau_ma3、metric_date 昇順。移動平均は小数第2位まで丸めてください。
| metric_date | dau |
|---|---|
| 2024-03-01 | 100 |
| 2024-03-02 | 120 |
| 2024-03-03 | 90 |
| 2024-03-04 | 150 |
| 2024-03-05 | 160 |
| 2024-03-06 | 130 |
| 2024-03-07 | 200 |
| metric_date | dau | dau_ma3 |
|---|---|---|
| 2024-03-01 | 100 | 100.00 |
| 2024-03-02 | 120 | 110.00 |
| 2024-03-03 | 90 | 103.33 |
| 2024-03-04 | 150 | 120.00 |
| 2024-03-05 | 160 | 133.33 |
| 2024-03-06 | 130 | 146.67 |
| 2024-03-07 | 200 | 163.33 |
SELECT metric_date, dau, ROUND( AVG(dau) OVER ( ORDER BY metric_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 現在行+直近2行=3行枠 ), 2 ) AS dau_ma3 FROM daily_active ORDER BY metric_date; /* 実行順序: 1. FROM daily_active → 行を読込 2. OVER (ORDER BY metric_date) → 日付順に並べる 3. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW → 現在行+2行前の枠を確定 4. AVG(dau) / ROUND(...,2) / ORDER BY → 枠内平均を丸めて出力 */
LEGEND
① FROM daily_active(7行)
FROM daily_active日次 DAU を全件読み込みます。03-03 の落ち込みと 03-07 の急増がノイズです。これを移動平均で平滑化します。| metric_date | dau |
|---|---|
| 03-01 | 100 |
| 03-02 | 120 |
| 03-03 | 90 |
| 03-04 | 150 |
| 03-05 | 160 |
| 03-06 | 130 |
| 03-07 | 200 |
ROWS BETWEEN n PRECEDING AND CURRENT ROW は現在行を含む直近 n+1 行の集合を対象にできます。AVG=移動平均、SUM=移動合計、MAX=直近の山と、集計関数を差し替えるだけで応用が利きます。WHERE 行番号 >= 3 等で端を除外します。ROWS BETWEEN 6 PRECEDING AND CURRENT ROW:枠の数字を変えるだけで任意窓に拡張できます。中央移動平均なら ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING のように未来側も含められます。RANGE UNBOUNDED PRECEDING になり、移動平均ではなく「先頭からの累積平均」になります。移動平均では必ず ROWS BETWEEN ... を明示してください。(SELECT AVG(dau) FROM t t2 WHERE t2.date BETWEEN ...) は行数の二乗のコストになり大規模データで破綻します。ウィンドウ関数なら1パスで計算できます。ファネル離脱率 — GROUP BY × LAG でステップ間コンバージョンを計算する
基礎編では COUNT(DISTINCT CASE WHEN...) で全ステップを横1行に並べました。応用編では縦持ち(1ステップ1行)に集計し、LAG() で「直前ステップ通過者」を引いてステップ間コンバージョンを求めます。ステップ数が増減してもクエリを書き換えずに済む、拡張性の高いパターンです。
CASE step WHEN 'visit' THEN 1 WHEN 'signup' THEN 2 ... END AS step_order -- 文字列の step は ABC順では正しい順にならない → 明示的な順序列を作る LAG(users) OVER (ORDER BY step_order) -- step_order の順で「1つ前のステップの通過者数」を取得 → 分母にする
activate, purchase, signup, visit のようにABC順に並び、ファネルが崩壊します。必ず数値の step_order を CASE で与えてから並べ替えます。funnel_events(生イベント)から、各ステップの通過ユニークユーザー数と「直前ステップからの通過率(step_cvr)」を縦持ちで出力してください。出力列は step, users, prev_users, step_cvr、ファネル順(step_order 昇順)。step_cvr は小数第2位まで。
| user_id | step |
|---|---|
| U1 | visit |
| U1 | signup |
| U1 | activate |
| U1 | purchase |
| U2 | visit |
| U2 | signup |
| U2 | activate |
| U3 | visit |
| U3 | signup |
| U4 | visit |
| U4 | signup |
| U5 | visit |
| U6 | visit |
| step | users | prev_users | step_cvr |
|---|---|---|---|
| visit | 6 | NULL | NULL |
| signup | 4 | 6 | 66.67 |
| activate | 2 | 4 | 50.00 |
| purchase | 1 | 2 | 50.00 |
WITH step_counts AS ( SELECT step, CASE step -- ファネル順を数値で明示 WHEN 'visit' THEN 1 WHEN 'signup' THEN 2 WHEN 'activate' THEN 3 WHEN 'purchase' THEN 4 END AS step_order, COUNT(DISTINCT user_id) AS users FROM funnel_events GROUP BY step ) SELECT step, users, LAG(users) OVER (ORDER BY step_order) AS prev_users, ROUND( users * 100.0 / NULLIF(LAG(users) OVER (ORDER BY step_order), 0), 2 ) AS step_cvr FROM step_counts ORDER BY step_order; /* 実行順序: 1. FROM funnel_events → 行を読込 2. GROUP BY step + COUNT(DISTINCT user_id) → ステップ別ユニーク数を集計 3. WITH step_counts → CTE 完成 4. LAG(users) OVER (ORDER BY step_order) → 前段の通過者数を取得 5. ステップ間通過率を計算 → 前段比で算出 */
LEGEND
① FROM funnel_events(13行)
FROM funnel_events生イベントを全件読み込みます。1ユーザーが複数ステップの行を持ちます。同一ユーザーの重複を避けるため COUNT(DISTINCT user_id) が必須です。| user_id | step |
|---|---|
| U1 | visit |
| U1 | signup |
| U1 | activate |
| U1 | purchase |
| U2 | visit |
| U2 | signup |
| U2 | activate |
| U3 | visit |
| U3 | signup |
| U4 | visit |
| U4 | signup |
| U5 | visit |
| U6 | visit |
GROUP BY で4行に集計された後のステップ列に適用されます。「集計 → ウィンドウ」の2段構えは、CTE で集計結果を作ってから window をかける定番の順序です。100 - step_cvr:通過率 66.67% の裏返しが離脱率 33.33% です。どのステップで何%が漏れるかを並べると、最大の改善余地(ここでは signup→activate の 50% 通過)が即座に分かります。ORDER BY step はABC順 activate→purchase→signup→visit になり、LAG が無関係なステップを引いて通過率が無意味になります。必ず数値の step_order を定義してから並べてください。JOIN ... ON 次ステップ時刻 BETWEEN 前ステップ時刻 AND 前ステップ時刻+7 のように実装します。日付スパイン — 再帰CTEで歯抜けの日を生成しゼロ埋めする
アクティビティが無い日は集計テーブルに行が存在しません。この「歯抜け」のままグラフ化すると日が詰まって見え、移動平均もズレます。再帰CTE(WITH RECURSIVE)で連続した日付の骨組み(日付スパイン)を生成し、実データを LEFT JOIN して COALESCE(..., 0) でゼロ埋めします。
WITH RECURSIVE date_spine AS ( SELECT DATE '2023-07-01' AS d -- ① アンカー: 開始の1行 UNION ALL SELECT d + 1 FROM date_spine -- ② 再帰項: 直前の行 +1 WHERE d < DATE '2023-07-07' -- ③ 停止条件: 終了日で打ち切り )
active_days(歯抜けの日次アクティブ数)から、2024-02-01〜02-05 の全日を再帰CTEで生成し、活動の無い日は 0 で埋めて出力してください。出力列は activity_date, active_users、日付昇順。
| activity_date | active_users |
|---|---|
| 2024-02-01 | 50 |
| 2024-02-02 | 65 |
| 2024-02-04 | 40 |
| activity_date | active_users |
|---|---|
| 2024-02-01 | 50 |
| 2024-02-02 | 65 |
| 2024-02-03 | 0 |
| 2024-02-04 | 40 |
| 2024-02-05 | 0 |
WITH RECURSIVE date_spine AS ( SELECT DATE '2024-02-01' AS d -- ① アンカー(1行) UNION ALL SELECT d + 1 -- ② 再帰項: 直前の d に +1 FROM date_spine WHERE d < DATE '2024-02-05' -- ③ 停止条件 ) SELECT s.d AS activity_date, COALESCE(a.active_users, 0) AS active_users -- 未マッチ日は 0 に FROM date_spine s LEFT JOIN active_days a ON a.activity_date = s.d ORDER BY s.d; /* 実行順序(再帰CTEの展開): 1. アンカー → 起点日を生成 2. 再帰 → 翌日を1日ずつ追加 3. 再帰終了 → 上限日で停止 4. date_spine LEFT JOIN active_days → 各日に実績を結合(無い日はNULL) 5. COALESCE(NULL,0) / ORDER BY s.d → 0補完して日付順出力 */
LEGEND
① 対象データ: active_days(3行・歯抜け)
テーブル確認: active_daysまず実データを確認します。02-01, 02-02, 02-04 のみ存在し、02-03 と 02-05 が抜けています。この歯抜けを埋めるために再帰CTEを使います。| activity_date | active_users |
|---|---|
| 2024-02-01 | 50 |
| 2024-02-02 | 65 |
| 2024-02-04 | 40 |
UNION ALL(重複排除しない)が必須で、UNION にすると余計な比較が入ります。FROM date_spine s LEFT JOIN active_days a の順序が肝です。「全部の日」を左、実データを右にすることで、活動ゼロの日も必ず1行残ります。逆順や INNER JOIN だと歯抜けが復活します。generate_series が定石:実務では generate_series(DATE '2024-02-01', DATE '2024-02-05', INTERVAL '1 day') の方が簡潔です。ただし再帰CTEは「親子階層の展開」「連番生成」「グラフ探索」など汎用ツールなので、構造を理解しておく価値があります。コホート・リテンション行列 — 経過週バケツ × FILTER でピボットする
基礎編は単一コホートの D1/D7 でした。応用編は複数コホート × 経過期間のリテンション行列(コホート・トライアングル)を作ります。鍵は2つ:(ログイン日 − コホート日) / 7 で「経過週」を算出すること、集計フィルタ FILTER (WHERE ...) で週ごとの列にピボットすることです。
(l.login_date - f.cohort_date) / 7 AS week_no -- PostgreSQL: DATE − DATE = 経過日数(整数)。/7 の整数除算で週番号に COUNT(DISTINCT user_id) FILTER (WHERE week_no = 1) -- FILTER: その集計関数だけに効く WHERE。週ごとの列を横に並べられる
COUNT(*) FILTER (WHERE 条件) は COUNT(CASE WHEN 条件 THEN 1 END) と等価ですが、意図が読みやすく、PostgreSQL の標準機能です。logins から、初回ログイン日でコホートを分け、経過週 0/1/2 のリテンション率の行列を出力してください。出力列は cohort_date, cohort_size, w0_pct, w1_pct, w2_pct、cohort_date 昇順。率は小数第2位まで。
| user_id | login_date |
|---|---|
| U1 | 2024-01-01 |
| U1 | 2024-01-08 |
| U1 | 2024-01-15 |
| U2 | 2024-01-01 |
| U2 | 2024-01-08 |
| U3 | 2024-01-01 |
| U4 | 2024-01-08 |
| U4 | 2024-01-15 |
| U5 | 2024-01-08 |
| cohort_date | cohort_size | w0_pct | w1_pct | w2_pct |
|---|---|---|---|---|
| 2024-01-01 | 3 | 100.00 | 66.67 | 33.33 |
| 2024-01-08 | 2 | 100.00 | 50.00 | 0.00 |
WITH first_login AS ( -- ① コホート日 = 初回ログイン日 SELECT user_id, MIN(login_date) AS cohort_date FROM logins GROUP BY user_id ), activity AS ( -- ② 経過週を算出 SELECT f.cohort_date, f.user_id, (l.login_date - f.cohort_date) / 7 AS week_no FROM first_login f JOIN logins l USING (user_id) ) SELECT cohort_date, COUNT(DISTINCT user_id) AS cohort_size, ROUND(COUNT(DISTINCT user_id) FILTER (WHERE week_no = 0) * 100.0 / COUNT(DISTINCT user_id), 2) AS w0_pct, ROUND(COUNT(DISTINCT user_id) FILTER (WHERE week_no = 1) * 100.0 / COUNT(DISTINCT user_id), 2) AS w1_pct, ROUND(COUNT(DISTINCT user_id) FILTER (WHERE week_no = 2) * 100.0 / COUNT(DISTINCT user_id), 2) AS w2_pct FROM activity GROUP BY cohort_date ORDER BY cohort_date; /* 実行順序: 1. first_login → 各ユーザーのコホート日を集計 2. activity → 週番号を算出 3. GROUP BY cohort_date → コホート別に集約 4. COUNT(DISTINCT user_id) FILTER → 週ごとの再訪率を算出 */
LEGEND
① FROM logins(9行)
FROM logins全ログイン履歴を読み込みます。U1 は3回、U4 は2回ログインしています。まず各ユーザーの初回ログイン日(コホート日)を決めます。| user_id | login_date |
|---|---|
| U1 | 01-01 |
| U1 | 01-08 |
| U1 | 01-15 |
| U2 | 01-01 |
| U2 | 01-08 |
| U3 | 01-01 |
| U4 | 01-08 |
| U4 | 01-15 |
| U5 | 01-08 |
(login_date - cohort_date)/7 で週、/30 で概月、/1 で日です。経過期間(period number)はコホート分析の縦軸であり、絶対日付ではなく「コホートからの相対経過」で揃えるのが要点です。COUNT(*) FILTER (WHERE week_no=1) は COUNT(CASE WHEN week_no=1 THEN 1 END) と等価ですが意図が明快です。横持ちの行列(週ごとの列)を作るときの第一選択です。パレート分析 — SUM OVER 累積と RANK で売上の集中度を測る
「売上の8割は上位2割の顧客から」── パレートの法則を SQL で定量化します。各顧客の売上構成比と、降順に積み上げた累積構成比を出し、累積構成比でABCランク分けします。鍵は2つのウィンドウ集計です。
SUM(revenue) OVER () -- フレーム無し・ORDER BY無しの OVER() = 全行の総合計(分母) SUM(revenue) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING) -- 先頭〜現在行までの累積(ランニング合計)。降順なので上位から積み上がる
WITH ranked AS (...) で列を確定させてから外側で CASE WHEN cum_share <= 80 ... と参照します。customer_revenue から、売上降順に「順位・構成比・累積構成比・ABCランク」を出力してください。出力列は customer_id, revenue, rev_rank, rev_share, cum_share, abc_class。ABCは累積構成比 ≤80%→A / ≤95%→B / それ超→C。率は小数第2位まで。
| customer_id | revenue |
|---|---|
| C1 | 5000 |
| C2 | 3000 |
| C3 | 1200 |
| C4 | 500 |
| C5 | 300 |
| customer_id | revenue | rev_rank | rev_share | cum_share | abc_class |
|---|---|---|---|---|---|
| C1 | 5000 | 1 | 50.00 | 50.00 | A |
| C2 | 3000 | 2 | 30.00 | 80.00 | A |
| C3 | 1200 | 3 | 12.00 | 92.00 | B |
| C4 | 500 | 4 | 5.00 | 97.00 | C |
| C5 | 300 | 5 | 3.00 | 100.00 | C |
WITH ranked AS ( SELECT customer_id, revenue, RANK() OVER (ORDER BY revenue DESC) AS rev_rank, ROUND(revenue * 100.0 / SUM(revenue) OVER (), 2) AS rev_share, -- 構成比 ROUND( SUM(revenue) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING) -- 上位からの累積 * 100.0 / SUM(revenue) OVER (), 2 ) AS cum_share FROM customer_revenue ) SELECT customer_id, revenue, rev_rank, rev_share, cum_share, CASE -- 累積構成比でABC分類 WHEN cum_share <= 80 THEN 'A' WHEN cum_share <= 95 THEN 'B' ELSE 'C' END AS abc_class FROM ranked ORDER BY rev_rank; /* 実行順序: 1. FROM customer_revenue → 行を読込 2. SUM(revenue) OVER () → 総合計を全行に付与 3. SUM(revenue) OVER (ORDER BY revenue DESC ...) → 上位からの累積を計算 4. rev_share / cum_share → 構成比と累積構成比を算出 5. 外側 CASE → A/B/C ランクに分類 */
LEGEND
① FROM customer_revenue(5行)
FROM customer_revenue顧客別売上を読み込みます。C1 が突出しています。これを売上降順に並べ、上位から累積していくと「どこまでで売上の8割に達するか」が見えます。| customer_id | revenue |
|---|---|
| C1 | 5000 |
| C2 | 3000 |
| C3 | 1200 |
| C4 | 500 |
| C5 | 300 |
OVER () は「全行の集計」を各行に配る:ORDER BY もフレームも無い空の OVER() はパーティション全体を対象にします。SUM(revenue) OVER () で総合計を全行に並べられるので、サブクエリ無しで構成比の分母が作れます。ROWS UNBOUNDED PRECEDING:移動平均が「直近N行」だったのに対し、累積は「先頭〜現在行」です。ROWS UNBOUNDED PRECEDING は ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW の短縮形で、ORDER BY を降順にすれば上位から積み上がるのがパレートの肝です。cum_share を CASE で参照できません(評価が同時のため)。WITH ranked で列を物理的に確定してから外側で参照するのが定石です。RANK/DENSE_RANK/ROW_NUMBER の使い分け(同順位の扱い)も押さえましょう。ORDER BY revenue(昇順)にすると小さい顧客から積み上がり、「上位2割で8割」という構造が読み取れません。必ず DESC で積んでください。ROWS を明示してください。同額の順位を一意にしたい場合は ORDER BY に tie-breaker(例: customer_id)を足します。