Day15 後半のゴール
「“現場でそのまま使える分析SQL”まで一段ギアを上げる」
前半では「売上集計+ランキング」の基本フォームを作りました。
後半では、それをもう一歩だけ実戦寄りにしていきます。
期間条件をパラメータっぽく扱う。
「売上ランキング+補助情報(割合・累積)」を出す。
CTEを重ねて“読みやすい分析SQL”に仕上げる。
ここまで来ると、「ダッシュボードの裏側に置けるSQL」のイメージにかなり近づきます。
例題1:期間をパラメータ化した商品別売上ランキング
「“直近30日”も“今年”も同じ型で書けるようにする」
まずは、前半で作った商品別売上ランキングに「期間」をちゃんと組み込みます。
ここでは「直近30日」を例にします。
WITH product_sales AS (
SELECT
product_id,
SUM(quantity * unit_price) AS sales_amount
FROM order_items
WHERE ordered_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY product_id
)
SELECT
product_id,
sales_amount,
RANK() OVER (ORDER BY sales_amount DESC) AS sales_rank
FROM product_sales
ORDER BY sales_rank
LIMIT 20;
SQLここでのポイントは、期間条件を「集計の中」に入れていることです。
WHERE ordered_at >= CURRENT_DATE – INTERVAL ’30 days’
→ 集計対象の行を「直近30日」に絞る。
この WHERE を、例えば「今年」に変えたければこうです。
WHERE ordered_at >= date_trunc('year', CURRENT_DATE)
SQL「期間を変えるだけで、同じ型のSQLがそのまま使える」状態を作るのが大事です。
現場では、アプリ側から「開始日・終了日」をパラメータで渡して、WHERE ordered_at BETWEEN :from AND :to のように書くことが多いです。
例題2:売上ランキングに“売上構成比”を足す
「ランキングだけでなく“全体の中での重さ”も見る」
単純なランキングだけだと、「1位がどれくらい強いのか」が分かりません。
そこで、「全体売上に対する割合(構成比)」を一緒に出してみます。
WITH product_sales AS (
SELECT
product_id,
SUM(quantity * unit_price) AS sales_amount
FROM order_items
WHERE ordered_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY product_id
),
total AS (
SELECT SUM(sales_amount) AS total_sales
FROM product_sales
)
SELECT
ps.product_id,
ps.sales_amount,
ROUND(ps.sales_amount / t.total_sales * 100, 2) AS sales_ratio_percent,
RANK() OVER (ORDER BY ps.sales_amount DESC) AS sales_rank
FROM product_sales ps
CROSS JOIN total t
ORDER BY sales_rank
LIMIT 20;
SQL流れを分解するとこうです。
product_sales
→ 商品別売上合計(直近30日)。
total
→ その期間の「全商品合計売上」。
最終SELECT
→ 各商品の売上を全体売上で割って、パーセントを出す。
CROSS JOIN total は、「全行に同じ total_sales をくっつける」ためのテクニックです。
これで、「1位の商品が全体の何%を占めているか」が一目で分かります。
例えば、結果がこうだったら:
1位:30%
2位:10%
3位:8%
「1位が圧倒的に強い市場だな」という解釈ができます。
ランキングに“意味”を持たせるために、構成比はかなり効きます。
例題3:売上ランキングに“累積構成比”を足す
「“上位何商品で全体の何%を占めるか”を見る」
さらに一歩進めて、「累積構成比」を出してみます。
これは、「上から順に足していったとき、どこまでで何%になるか」を見る指標です。
さっきのSQLに、ウィンドウ関数を1つ足します。
WITH product_sales AS (
SELECT
product_id,
SUM(quantity * unit_price) AS sales_amount
FROM order_items
WHERE ordered_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY product_id
),
total AS (
SELECT SUM(sales_amount) AS total_sales
FROM product_sales
),
ranked AS (
SELECT
ps.product_id,
ps.sales_amount,
ps.sales_amount / t.total_sales AS sales_ratio,
RANK() OVER (ORDER BY ps.sales_amount DESC) AS sales_rank
FROM product_sales ps
CROSS JOIN total t
)
SELECT
product_id,
sales_amount,
ROUND(sales_ratio * 100, 2) AS sales_ratio_percent,
ROUND(
SUM(sales_ratio) OVER (ORDER BY sales_rank
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) * 100,
2
) AS cumulative_ratio_percent,
sales_rank
FROM ranked
ORDER BY sales_rank
LIMIT 20;
SQLここでのキモは、この部分です。
SUM(sales_ratio) OVER (
ORDER BY sales_rank
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
SQLこれは、「売上比率(sales_ratio)を、ランキング順に上から累積していく」ウィンドウ関数です。
結果イメージ:
1位:売上比率 30%、累積 30%
2位:売上比率 10%、累積 40%
3位:売上比率 8%、累積 48%
…
「上位3商品で全体の48%を占めている」といった読み方ができます。
これは、いわゆる「パレート分析(80:20)」の入口です。
「上位何商品で80%に到達するか」を見ることで、
「どこまでを重点商品として扱うか」の判断材料になります。
例題4:ユーザー別売上ランキング+“優良顧客候補”のフラグ
「ランキング結果を“セグメント”に落とす」
今度はユーザー別売上ランキングに、「優良顧客候補」のフラグを付けてみます。
例えば、「直近90日で上位10%に入っているユーザー」を優良候補とする、というルールを仮定します。
WITH user_sales AS (
SELECT
user_id,
SUM(quantity * unit_price) AS sales_amount
FROM order_items
WHERE ordered_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY user_id
),
ranked AS (
SELECT
user_id,
sales_amount,
RANK() OVER (ORDER BY sales_amount DESC) AS sales_rank,
COUNT(*) OVER () AS user_count
FROM user_sales
)
SELECT
user_id,
sales_amount,
sales_rank,
CASE
WHEN sales_rank <= user_count * 0.1 THEN true
ELSE false
END AS is_premium_candidate
FROM ranked
ORDER BY sales_rank
LIMIT 100;
SQLここでのポイントは2つです。
COUNT(*) OVER () で「ユーザー総数」を、全行に持たせている。sales_rank <= user_count * 0.1 で「上位10%以内かどうか」を判定している。
これで、「売上ランキングの上位10%に入っているユーザー」に is_premium_candidate = true が立ちます。
単なるランキングを、「セグメント(優良候補/それ以外)」に落とし込むことで、
マーケティング施策や通知の対象を決める材料になります。
例題5:分析SQLを“読みやすくする”CTEの分割
「1CTE=1ステップ」にすると、あとから見ても怖くない
ここまでの例を見て、「CTE多くない?」と思ったかもしれません。
でも、分析SQLは「1CTE=1ステップ」と割り切った方が、長期的には読みやすくなります。
例えば、さっきの「商品別売上ランキング+構成比+累積構成比」は、
ざっくりこういうステップに分かれています。
product_sales
→ 商品別売上合計(期間条件込み)。
total
→ 全体売上。
ranked
→ 売上比率とランキングを付ける。
最終SELECT
→ 累積比率を計算して、表示用に整形する。
この「ステップの名前」が、そのままSQLの可読性になります。
CTEをケチって1つのSELECTに全部詰め込むと、
「何をやっているのか」が一気に分かりにくくなり、
バグっても直しづらくなります。
分析SQLは、「多少長くても、意味が追えること」の方が圧倒的に大事です。
Day15 の中間課題では、「CTEを遠慮なく使って、手順をそのままSQLにする」感覚を持ってほしいです。
Day15 後半のまとめ
中間課題レベルの分析SQLでは、「期間条件を WHERE にきちんと組み込み、商品別・ユーザー別などの粒度で売上合計を出すCTEを作り、その上に RANK() を重ねてランキングを付ける」だけでなく、「全体売上を別CTEで計算して構成比(sales_amount / total_sales)を出し、さらに SUM(sales_ratio) OVER (ORDER BY sales_rank ...) で累積構成比を計算する」といった“意味のある指標”まで一気に出してしまうのが実戦的になる。
ユーザー別ランキングに COUNT(*) OVER () を組み合わせて「上位10%に入っているかどうか」のフラグを付ければ、単なる順位表が「優良顧客候補セグメント」に変わり、CTEを「1ステップ=1意味」で分割することで、SQL自体が“分析の手順書”として読めるようになる。
Day15 後半の着地点は、「売上・ランキングSQLを、期間・構成比・累積・セグメントまで含めて、CTE+ウィンドウ関数で素直に組み立てられる自分の型を持つこと」。

