SQL KPI分析 — ファネル・コホート・パレートの応用

応用KPI分析移動平均ファネル離脱率コホート行列パレート分析PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

DAU移動平均 — ウィンドウフレーム ROWS BETWEEN で N日移動平均を計算する

AVG OVERROWS BETWEEN移動平均ウィンドウフレーム
前提知識

日次 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 は別物ROWS は物理的な行数で枠を決めます。RANGE は ORDER BY 値が同じ行(ピア)をまとめて扱うため、日付に重複があると意図せず多くの行が枠に入ります。移動平均は行数で数える ROWS を使うのが定石です。
問題

daily_active テーブルから、各日の DAU と「3日移動平均(dau_ma3)」を計算してください。出力列は metric_date, dau, dau_ma3、metric_date 昇順。移動平均は小数第2位まで丸めてください。

使用テーブル
► daily_active(7行)
metric_datedau
2024-03-01100
2024-03-02120
2024-03-0390
2024-03-04150
2024-03-05160
2024-03-06130
2024-03-07200
期待出力
metric_datedaudau_ma3
2024-03-01100100.00
2024-03-02120110.00
2024-03-0390103.33
2024-03-04150120.00
2024-03-05160133.33
2024-03-06130146.67
2024-03-07200163.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        → 枠内平均を丸めて出力
  */
解説(テーブル変化・ポイント)
SELECT metric_date, dau, ROUND( AVG(dau) OVER ( ORDER BY metric_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ), 2 ) AS dau_ma3 FROM daily_active ORDER BY metric_date;
LEGEND
データ取得・読込対象
① FROM daily_active(7行)
FROM daily_active日次 DAU を全件読み込みます。03-03 の落ち込みと 03-07 の急増がノイズです。これを移動平均で平滑化します。
1 / 6
metric_datedau
03-01100
03-02120
03-0390
03-04150
03-05160
03-06130
03-07200
7行読込
学習ポイント
ウィンドウフレームは「行の集合」を集計対象にする:LAG/LEAD は1点だけですが、ROWS BETWEEN n PRECEDING AND CURRENT ROW は現在行を含む直近 n+1 行の集合を対象にできます。AVG=移動平均、SUM=移動合計、MAX=直近の山と、集計関数を差し替えるだけで応用が利きます。
端(先頭)では枠が自動的に縮む:03-01 は2行前が存在しないため枠は1行だけ、03-02 は2行です。NULL にはならず「存在する行だけ」で平均します。厳密な3日平均が必要なら WHERE 行番号 >= 3 等で端を除外します。
7日移動平均は ROWS BETWEEN 6 PRECEDING AND CURRENT ROW:枠の数字を変えるだけで任意窓に拡張できます。中央移動平均なら ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING のように未来側も含められます。
アンチパターン
フレーム句を省略して RANGE 既定が効いてしまう:ORDER BY 付き OVER でフレームを書かないと既定は RANGE UNBOUNDED PRECEDING になり、移動平均ではなく「先頭からの累積平均」になります。移動平均では必ず ROWS BETWEEN ... を明示してください。
自己結合や相関サブクエリで移動平均を組む(SELECT AVG(dau) FROM t t2 WHERE t2.date BETWEEN ...) は行数の二乗のコストになり大規模データで破綻します。ウィンドウ関数なら1パスで計算できます。
実務コラム:移動平均と WoW の役割分担
移動平均はトレンド(地合い)を見る道具、前週比 WoW は変化点を見る道具です。実務では「7日移動平均の折れ線」+「WoW のバー」を同じダッシュボードに並べ、平滑線で方向感、バーで異常検知という二段構えにするのが定番です。曜日効果が強い B2C は7日窓、B2B は平日のみの5日窓が好まれます。
QUESTION 2

ファネル離脱率 — GROUP BY × LAG でステップ間コンバージョンを計算する

GROUP BYLAG OVERファネルステップ順序NULLIF
前提知識

基礎編では 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つ前のステップの通過者数」を取得 → 分母にする
ステップの順序は自分で定義する:step を文字列のまま ORDER BY すると activate, purchase, signup, visit のようにABC順に並び、ファネルが崩壊します。必ず数値の step_order を CASE で与えてから並べ替えます。
問題

funnel_events(生イベント)から、各ステップの通過ユニークユーザー数と「直前ステップからの通過率(step_cvr)」を縦持ちで出力してください。出力列は step, users, prev_users, step_cvr、ファネル順(step_order 昇順)。step_cvr は小数第2位まで。

使用テーブル
► funnel_events(13行)
user_idstep
U1visit
U1signup
U1activate
U1purchase
U2visit
U2signup
U2activate
U3visit
U3signup
U4visit
U4signup
U5visit
U6visit
期待出力
stepusersprev_usersstep_cvr
visit6NULLNULL
signup4666.67
activate2450.00
purchase1250.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. ステップ間通過率を計算                              → 前段比で算出
  */
解説(テーブル変化・ポイント)
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;
LEGEND
データ取得・読込対象
① FROM funnel_events(13行)
FROM funnel_events生イベントを全件読み込みます。1ユーザーが複数ステップの行を持ちます。同一ユーザーの重複を避けるため COUNT(DISTINCT user_id) が必須です。
1 / 6
user_idstep
U1visit
U1signup
U1activate
U1purchase
U2visit
U2signup
U2activate
U3visit
U3signup
U4visit
U4signup
U5visit
U6visit
13行読込
学習ポイント
縦持ち+LAG はステップ数に強い:基礎編の横持ち(CASE WHEN を列ごとに書く)はステップ追加のたびに列を増やす必要がありますが、縦持ち+LAG ならデータが増えるだけでクエリは不変です。BI ツールの折れ線・ファネルチャートにも縦持ちが直結します。
ウィンドウ関数は集計後の結果に対しても効く:LAG は GROUP BY で4行に集計された後のステップ列に適用されます。「集計 → ウィンドウ」の2段構えは、CTE で集計結果を作ってから window をかける定番の順序です。
離脱率は 100 - step_cvr:通過率 66.67% の裏返しが離脱率 33.33% です。どのステップで何%が漏れるかを並べると、最大の改善余地(ここでは signup→activate の 50% 通過)が即座に分かります。
アンチパターン
step(文字列)でそのまま ORDER BY するORDER BY step はABC順 activate→purchase→signup→visit になり、LAG が無関係なステップを引いて通過率が無意味になります。必ず数値の step_order を定義してから並べてください。
分母を「最初のステップ固定」にする:全ステップを visit で割ると「全体到達率」になり、どのステップ間で漏れたかが分かりません。ボトルネック特定には直前ステップを分母にしたステップ間 CVR が必要です。
実務コラム:ファネルの時間窓と順序制約
本問は「ステップ通過の有無」だけを見ていますが、実務では「visit から7日以内の signup だけを通過と認める」といった時間窓や、「signup の後に来た activate のみ有効」という順序制約を課すことが多いです。これらはイベントに時刻を持たせ、CTE でユーザーごとの初回ステップ時刻を求めてから JOIN ... ON 次ステップ時刻 BETWEEN 前ステップ時刻 AND 前ステップ時刻+7 のように実装します。
QUESTION 3

日付スパイン — 再帰CTEで歯抜けの日を生成しゼロ埋めする

WITH RECURSIVEUNION ALL日付スパインゼロ埋めCOALESCE
前提知識

アクティビティが無い日は集計テーブルに行が存在しません。この「歯抜け」のままグラフ化すると日が詰まって見え、移動平均もズレます。再帰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'     -- ③ 停止条件: 終了日で打ち切り
)
再帰CTEの3要素:アンカー(初期行)・再帰項(直前の結果を参照して次行を生成)・停止条件(WHERE)。停止条件を誤ると無限ループになるため、必ず「いつか偽になる」条件を置きます。
問題

active_days(歯抜けの日次アクティブ数)から、2024-02-01〜02-05 の全日を再帰CTEで生成し、活動の無い日は 0 で埋めて出力してください。出力列は activity_date, active_users、日付昇順。

使用テーブル
► active_days(3行・歯抜け)
activity_dateactive_users
2024-02-0150
2024-02-0265
2024-02-0440
期待出力
activity_dateactive_users
2024-02-0150
2024-02-0265
2024-02-030
2024-02-0440
2024-02-050
模範解答コード
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補完して日付順出力
  */
解説(テーブル変化・ポイント)
WITH RECURSIVE date_spine AS ( SELECT DATE '2024-02-01' AS d UNION ALL SELECT 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 FROM date_spine s LEFT JOIN active_days a ON a.activity_date = s.d ORDER BY s.d;
LEGEND
データ取得・読込対象
① 対象データ: active_days(3行・歯抜け)
テーブル確認: active_daysまず実データを確認します。02-01, 02-02, 02-04 のみ存在し、02-03 と 02-05 が抜けています。この歯抜けを埋めるために再帰CTEを使います。
1 / 8
activity_dateactive_users
2024-02-0150
2024-02-0265
2024-02-0440
3行(元データ)
学習ポイント
再帰CTEは「直前の結果を入力に次を作る」反復:アンカーが1回目の入力、再帰項は前回追加された行だけを入力に次行を生成します。WHERE が偽になり新規行が0件になった時点で停止します。UNION ALL(重複排除しない)が必須で、UNION にすると余計な比較が入ります。
骨組み(スパイン)を左に置いて LEFT JOIN するFROM date_spine s LEFT JOIN active_days a の順序が肝です。「全部の日」を左、実データを右にすることで、活動ゼロの日も必ず1行残ります。逆順や INNER JOIN だと歯抜けが復活します。
PostgreSQL では generate_series が定石:実務では generate_series(DATE '2024-02-01', DATE '2024-02-05', INTERVAL '1 day') の方が簡潔です。ただし再帰CTEは「親子階層の展開」「連番生成」「グラフ探索」など汎用ツールなので、構造を理解しておく価値があります。
アンチパターン
停止条件を書き忘れ無限ループにする:再帰項に WHERE が無い、または「いつまでも真」の条件だと行が無限に生成されます。必ず「終了日で偽になる」条件を置き、可能なら再帰回数の上限も設けてください。
歯抜けのまま移動平均や前日比を計算する:02-03 の行が無いと、02-04 の「前日」が 02-02 になり前日比がズレ、移動平均の窓もずれます。時系列KPIは必ず日付スパインで連続化してから window をかけてください。
実務コラム:日付スパインはKPI基盤の土台
多くの DWH では dim_date(日付ディメンション)という「全日付+曜日・祝日・四半期フラグ」を持つマスタを事前に用意し、ファクトを LEFT JOIN します。これにより「活動ゼロの日」「曜日別集計」「営業日のみの集計」がすべて JOIN だけで実現できます。再帰CTE / generate_series はこの dim_date を作る最初の一歩です。
QUESTION 4

コホート・リテンション行列 — 経過週バケツ × FILTER でピボットする

多段CTEFILTERコホート行列日付差バケツ
前提知識

基礎編は単一コホートの 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。週ごとの列を横に並べられる
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位まで。

使用テーブル
► logins(9行)
user_idlogin_date
U12024-01-01
U12024-01-08
U12024-01-15
U22024-01-01
U22024-01-08
U32024-01-01
U42024-01-08
U42024-01-15
U52024-01-08
期待出力
cohort_datecohort_sizew0_pctw1_pctw2_pct
2024-01-013100.0066.6733.33
2024-01-082100.0050.000.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  → 週ごとの再訪率を算出
  */
解説(テーブル変化・ポイント)
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) ... ) AS w1_pct, ROUND( ... FILTER (WHERE week_no = 2) ... ) AS w2_pct FROM activity GROUP BY cohort_date ORDER BY cohort_date;
LEGEND
データ取得・読込対象
① FROM logins(9行)
FROM logins全ログイン履歴を読み込みます。U1 は3回、U4 は2回ログインしています。まず各ユーザーの初回ログイン日(コホート日)を決めます。
1 / 7
user_idlogin_date
U101-01
U101-08
U101-15
U201-01
U201-08
U301-01
U401-08
U401-15
U501-08
9行読込
学習ポイント
多段CTEで「定義」を積み上げる:first_login がコホートの定義、activity が経過週の定義、外側が集計と、責務を分けています。読みやすく、コホート基準を「初回購入日」等に変えても CTE を差し替えるだけで済みます。
日付差バケツ化で任意粒度の「経過期間」を作る(login_date - cohort_date)/7 で週、/30 で概月、/1 で日です。経過期間(period number)はコホート分析の縦軸であり、絶対日付ではなく「コホートからの相対経過」で揃えるのが要点です。
FILTER は CASE WHEN より読みやすいピボットCOUNT(*) FILTER (WHERE week_no=1)COUNT(CASE WHEN week_no=1 THEN 1 END) と等価ですが意図が明快です。横持ちの行列(週ごとの列)を作るときの第一選択です。
アンチパターン
分母にコホートサイズではなく「前週の人数」を使う:コホート・リテンションの分母は常にコホート初期サイズ(w0の人数)です。前週を分母にすると「継続率の継続率」になり別物の指標になります。定義を取り違えると経営判断を誤ります。
JOIN を LEFT にせず再訪0の週が欠ける:本問は全コホートで w0 が必ず存在するため JOIN で足りますが、「ある週に誰も再訪しなかった」ケースを 0 として明示したい場合は、週スパイン(Q3)を CROSS JOIN してから LEFT JOIN するのが堅牢です。
実務コラム:コホート・トライアングルの読み方
行をコホート、列を経過週にした三角行列をコホート・トライアングルと呼びます。縦に見れば「新しいコホートほど定着が良いか(プロダクト改善の効果)」、横に見れば「どの週で離脱が止まり下げ止まるか(リテンションの底)」が分かります。下げ止まる水準が高いほど PMF(プロダクトマーケットフィット)に近いと判断されます。
QUESTION 5

パレート分析 — SUM OVER 累積と RANK で売上の集中度を測る

SUM OVER ()累積フレームパレート分析ABC分析RANK
前提知識

「売上の8割は上位2割の顧客から」── パレートの法則を SQL で定量化します。各顧客の売上構成比と、降順に積み上げた累積構成比を出し、累積構成比でABCランク分けします。鍵は2つのウィンドウ集計です。

SUM(revenue) OVER ()
-- フレーム無し・ORDER BY無しの OVER() = 全行の総合計(分母)

SUM(revenue) OVER (ORDER BY revenue DESC ROWS UNBOUNDED PRECEDING)
-- 先頭〜現在行までの累積(ランニング合計)。降順なので上位から積み上がる
同じ SELECT 内ではウィンドウの別名を再利用できない:cum_share を ABC 判定に使い回すには、いったん 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_revenue(5行)
customer_idrevenue
C15000
C23000
C31200
C4500
C5300
期待出力
customer_idrevenuerev_rankrev_sharecum_shareabc_class
C15000150.0050.00A
C23000230.0080.00A
C31200312.0092.00B
C450045.0097.00C
C530053.00100.00C
模範解答コード
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 ランクに分類
  */
解説(テーブル変化・ポイント)
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 WHEN cum_share <= 80 THEN 'A' WHEN cum_share <= 95 THEN 'B' ELSE 'C' END AS abc_class FROM ranked ORDER BY rev_rank;
LEGEND
データ取得・読込対象
① FROM customer_revenue(5行)
FROM customer_revenue顧客別売上を読み込みます。C1 が突出しています。これを売上降順に並べ、上位から累積していくと「どこまでで売上の8割に達するか」が見えます。
1 / 7
customer_idrevenue
C15000
C23000
C31200
C4500
C5300
5行読込
学習ポイント
OVER () は「全行の集計」を各行に配る:ORDER BY もフレームも無い空の OVER() はパーティション全体を対象にします。SUM(revenue) OVER () で総合計を全行に並べられるので、サブクエリ無しで構成比の分母が作れます。
累積は ROWS UNBOUNDED PRECEDING:移動平均が「直近N行」だったのに対し、累積は「先頭〜現在行」です。ROWS UNBOUNDED PRECEDINGROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW の短縮形で、ORDER BY を降順にすれば上位から積み上がるのがパレートの肝です。
ウィンドウ別名はその場で再利用できない → CTE で確定させる:同一 SELECT 内では cum_share を CASE で参照できません(評価が同時のため)。WITH ranked で列を物理的に確定してから外側で参照するのが定石です。RANK/DENSE_RANK/ROW_NUMBER の使い分け(同順位の扱い)も押さえましょう。
アンチパターン
累積で ORDER BY を昇順にする:パレートは大きい順に積むのが前提です。ORDER BY revenue(昇順)にすると小さい顧客から積み上がり、「上位2割で8割」という構造が読み取れません。必ず DESC で積んでください。
累積フレームを省略して既定の RANGE に頼る:同額の売上が複数あると、既定 RANGE は同額行(ピア)をまとめて加算し累積が飛びます。1行ずつ正確に積むには ROWS を明示してください。同額の順位を一意にしたい場合は ORDER BY に tie-breaker(例: customer_id)を足します。
実務コラム:ABC分析と在庫・顧客戦略
ABC分析は売上だけでなく在庫管理(重点管理するSKUの選定)や顧客サクセス(手厚く担当を付ける顧客の選定)に広く使われます。A群は売上貢献が大きく離反インパクトも大きいので人的リソースを集中、C群は自動化・省力運用に回すのが定石です。同じクエリの ORDER BY と分類対象を「商品×粗利」「地域×件数」に変えるだけで多様な集中度分析に転用できます。