SQL GROUP BY — COUNT DISTINCT・FILTER・ROLLUPの応用

応用GROUP BYCOUNT DISTINCT条件付き集計 / FILTERROLLUP / 二段階集計構成比PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

ユニーク数 — COUNT(DISTINCT) でグループ内の重複を除いて数える

COUNT(DISTINCT)ユニーク数重複を除く集計ユニーク集計
前提知識

同じ人が何度もアクセスする——そんなデータで「延べ件数」と「実人数」は別物です。グループ内の重複を除いた個数を数えるには COUNT(DISTINCT 列) を使います。COUNT(*)(行数)と COUNT(列)(NULL以外の数)との違いが鍵です。

-- 3種類の COUNT を比べる
COUNT(*)                  -- 行数(NULLも含めて全行)
COUNT(user_id)           -- user_id が NULL でない行数
COUNT(DISTINCT user_id)  -- user_id の異なる値の数(重複を1つに畳む)
DISTINCT は「異なる値の数」:COUNT(DISTINCT user_id) は、グループ内で重複を除いた user_id の種類数を返します。延べアクセス(COUNT(*))が同じでも、同一ユーザーの再訪が多ければ実人数(COUNT(DISTINCT user_id))は小さくなります。NULL は数えません。
問題

page_views テーブルから、日別の延べ閲覧数とユニーク訪問者数を求めてください。出力列は view_date, total_views, unique_users。view_date の昇順で返してください。

使用テーブル
► page_views(7行)
view_idview_dateuser_id
12024-05-01U1
22024-05-01U1
32024-05-01U2
42024-05-02U2
52024-05-02U3
62024-05-02U3
72024-05-02U1
期待出力
view_datetotal_viewsunique_users
2024-05-0132
2024-05-0243
QUESTION 7

条件付き集計 — FILTER でステータス別に横持ち集計(ピボット)する

FILTER句条件付き集計ピボット/横持ちクロス集計
前提知識

「状態ごとの件数を1行に並べたい」——縦に積まれた status を横方向の列に展開するのがピボット(横持ち)です。条件付き集計を使えば、テーブルを1回スキャンするだけで複数の指標を同時に取れます。PostgreSQL では集計関数の FILTER (WHERE …) がエレガントです。

-- FILTER は「その集計だけに効く WHERE」
COUNT(*) FILTER (WHERE status = 'paid')      -- paid の行だけ数える
SUM(amount) FILTER (WHERE status = 'paid') -- paid の金額だけ合計
-- 移植性重視なら CASE で同等に書ける
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END)
WHERE と FILTER の決定的な違い:WHERE はグループ全体の行を絞り込むため、すべての集計に同じ条件がかかります。一方 FILTERその集計関数1つだけに条件を効かせます。だから「総数」と「paid だけの数」を同じ行に並べて出せるのです。条件に当たる行が無ければ COUNT は0、SUM は NULL になります。
問題

orders テーブルから、カテゴリ別に、総件数・ステータス別件数・paid の合計金額を1行にまとめてください。出力列は category, total_orders, paid_orders, pending_orders, cancelled_orders, paid_amount。total_orders の降順、同数なら category の昇順で返してください。

使用テーブル
► orders(8行)
order_idcategorystatusamount
1Bookspaid1200
2Bookspaid800
3Bookspaid600
4Bookspending500
5Bookscancelled300
6Toyspaid2000
7Toyscancelled1500
8Toyspending700
期待出力
categorytotal_orderspaid_orderspending_orderscancelled_orderspaid_amount
Books53112600
Toys31112000
QUESTION 8

小計と総計 — ROLLUP で小計・総計行を一度に出す

GROUP BY ROLLUPGROUPING()小計/総計集計の階層化
前提知識

明細の集計に加えて「地域ごとの小計」と「全体の総計」も同じ表に出したい——そんなときは GROUP BY ROLLUP(…) です。通常のグループ化に、上位のまとめ行(小計・総計)を自動で付け足してくれます。まとめ行かどうかは GROUPING() で見分けます。

-- ROLLUP(a, b) は次の3通りの集計を一度に生成する
GROUP BY ROLLUP(region, product)
--   (region, product) … 明細
--   (region)          … region 小計(product を畳む)
--   ()                … 総計(すべて畳む)

GROUPING(product)  -- その行で product が畳まれていれば 1、明細なら 0
「畳まれた列」は NULL になる:ROLLUP が生成する小計・総計行では、畳んだ列の値が NULL になります。ただし元データの NULL と区別がつかないため、GROUPING(列)=1(=その列はまとめられた)で判定し、CASE'(All)' などのラベルに置き換えるのが定番です。
問題

sales テーブルから、地域×商品の売上に、地域ごとの小計と全体の総計を加えた表を作ってください。出力列は region, product, total_amount。小計・総計の行では畳んだ列を (All) と表示し、各ブロックの末尾(明細→小計、最後に総計)に並ぶようにしてください。

使用テーブル
► sales(4行)
regionproductamount
EastApple100
EastBanana150
WestApple200
WestBanana250
期待出力
regionproducttotal_amount
EastApple100
EastBanana150
East(All)250
WestApple200
WestBanana250
West(All)450
(All)(All)700
QUESTION 9

二段階集計 — 派生テーブルで「平均の平均」を正しく出す

サブクエリ/CTE二段階集計粒度の変換集計の集計
前提知識

「顧客あたりの平均購入額」を出すつもりが、いつのまにか「注文あたりの平均」になっていた——集計の粒度を誤る典型です。ある集計の結果をさらに集計するには、サブクエリ(派生テーブル)や CTE で段階を分けます。1つの GROUP BY だけでは表現できません。

-- ① まず顧客ごとに合計(粒度=顧客)
WITH customer_totals AS (
  SELECT customer_id, SUM(amount) AS customer_total
  FROM orders GROUP BY customer_id
)
-- ② その結果(1行=1顧客)を、さらに平均する
SELECT AVG(customer_total) FROM customer_totals;
粒度が変わると意味が変わる:生の orders に対する AVG(amount)注文あたりの平均です。一度 GROUP BY customer_id顧客の合計に畳んでから AVG すると、顧客あたりの平均になります。両者は別の数値。「何あたりの平均か」を常に意識しましょう。
問題

orders テーブルから、地域ごとに「顧客数」と「顧客1人あたりの平均購入額」を求めてください(まず顧客ごとの合計を出し、その合計を地域で平均します)。出力列は region, customer_count, avg_customer_spend(平均は四捨五入して整数)。avg_customer_spend の降順、同数なら region の昇順で返してください。

使用テーブル
► orders(6行)
order_idregioncustomer_idamount
1EastC11000
2EastC12000
3EastC23000
4WestC3500
5WestC3500
6WestC42000
期待出力
regioncustomer_countavg_customer_spend
East23000
West21500
QUESTION 10

構成比 — ウィンドウ集約で各グループの全体シェアを出す

ウィンドウ関数SUM() OVER ()構成比/シェア集計の上の集計
前提知識

グループ別の合計に「全体の何%か」を併記したい——ウィンドウ関数 SUM(…) OVER () を使うと、GROUP BY の結果行を保ったまま、全体の集計値を各行にブロードキャストできます。サブクエリや結合なしで構成比が1文で出せます。

-- 各グループ合計を、全グループの総計で割る
SUM(amount)                       -- ① GROUP BY 後:このグループの合計
SUM(SUM(amount)) OVER ()           -- ② ①をグループ横断で合計=総計(全行に同じ値)
100.0 * SUM(amount) / SUM(SUM(amount)) OVER ()  -- ③ 構成比(%)
SUM(SUM(x)) OVER () の二重集計:内側の SUM(amount) は GROUP BY 後の各グループ合計。それを外側のウィンドウ SUM(…) OVER ()グループをまたいで合計し、総計を全行へ配ります。ウィンドウ関数は集計の後段で動くため、集計結果の上にもう一段の集計を重ねられるのです。OVER () は「全行が1つの窓」を意味します。
問題

sales テーブルから、カテゴリ別の売上合計と、それが全体に占める構成比(%)を求めてください。出力列は category, category_total, pct_of_total(構成比は小数第1位に四捨五入)。category_total の降順、同額なら category の昇順で返してください。

使用テーブル
► sales(5行)
sale_idcategoryamount
1Electronics4000
2Electronics2000
3Clothing1500
4Clothing500
5Food2000
期待出力
categorycategory_totalpct_of_total
Electronics600060.0
Clothing200020.0
Food200020.0