カテゴリ別の売上構成比を出します。サブクエリで全体合計を持ってくる方法と、SUM() OVER で店舗ごとの分母に切り替える方法の 2 通りを練習します。
daily_sales に商品カテゴリは入っていません。 products と結合して取ってきます。
SELECT p.category, SUM(ds.amount) AS sales
FROM daily_sales ds
JOIN products p ON p.id = ds.product_id
GROUP BY p.category
ORDER BY sales DESC;
daily_sales.amount は 単価 × 数量をあらかじめ入れてある列です。
そのため SUM(p.price * ds.quantity) と書いても同じ答えになります。
実務のテーブルでも、 集計を軽くするために金額を持たせておくことはよくあります。
「電子機器が 1451 万」 と言われてもピンときません。 全体の何%かに直します。
分母 (全体の合計) は 1 行だけの値なので、 スカラーサブクエリで取ってこられます。
SELECT p.category,
SUM(ds.amount) AS sales,
ROUND(SUM(ds.amount) * 100.0 / (SELECT SUM(amount) FROM daily_sales), 1) AS pct
FROM daily_sales ds
JOIN products p ON p.id = ds.product_id
GROUP BY p.category
ORDER BY sales DESC;
100.0 と小数で書くのが要点です。 100 と整数で書くと
整数どうしの割り算になって小数が切り捨てられ、 ほとんどが 0 になります。
「店舗ごとに、 その店の中での構成比」 が欲しいときは、 分母が店舗ごとに変わります。
サブクエリだと店舗の数だけ書き分けることになり、 現実的ではありません。
ここで 集計関数に OVER を付ける書き方が効きます。
SELECT ds.store, p.category,
SUM(ds.amount) AS sales,
ROUND(SUM(ds.amount) * 100.0
/ SUM(SUM(ds.amount)) OVER (PARTITION BY ds.store), 1) AS pct
FROM daily_sales ds
JOIN products p ON p.id = ds.product_id
GROUP BY ds.store, p.category
ORDER BY ds.store, sales DESC;
SUM(SUM(...)) OVER (...) という二重の SUM に驚くかもしれませんが、
順番に考えると素直です。
1. 内側の SUM(ds.amount) … GROUP BY で店舗×カテゴリの合計が出る (6 行になる)
2. 外側の SUM(...) OVER (PARTITION BY ds.store) … その 6 行を店舗ごとに足す
ウィンドウ関数は GROUP BY の後に評価されるので、 集計済みの行に対して
さらに集計をかけられます。 この順番を覚えておくと、 二重 SUM は自然に読めます。
構成比を金額で見ると 文房具はわずか 3.6% です。 ところが数量で見ると、
いちばん売れているのはペンとノートです。
SELECT p.name, SUM(ds.quantity) AS q, SUM(ds.amount) AS sales
FROM daily_sales ds
JOIN products p ON p.id = ds.product_id
GROUP BY p.name
ORDER BY q DESC;
「売れている」 が金額なのか個数なのかで、 出す施策が変わります。
構成比を出すときは、 何を分子に置いたのかを必ず列名に残しておきます。
- pct が全部 0 になる — 100 を整数で書いています。 100.0 にします。
- 合計しても 100% にならない — 四捨五入の誤差です。 100.1% や 99.9% は正常で、
ぴったり合わせたい場合は最大の行で調整するのが一般的です。
- OVER の中に PARTITION BY を書き忘れる — 分母が全体になり、
「店舗ごとの構成比」 のつもりが 「全社の中での構成比」 になります。
売上構成比は 「どこに手を入れるか」 を決めるための地図です。 店舗ごと ・ 月ごとに
同じ構成比を並べると、 全体の伸びではなく 中身の入れ替わりが見えます。
次のレッスン: 移動平均 — 日々のブレをならす