ユニーク数 — COUNT(DISTINCT) でグループ内の重複を除いて数える
同じ人が何度もアクセスする——そんなデータで「延べ件数」と「実人数」は別物です。グループ内の重複を除いた個数を数えるには COUNT(DISTINCT 列) を使います。COUNT(*)(行数)と COUNT(列)(NULL以外の数)との違いが鍵です。
-- 3種類の COUNT を比べる COUNT(*) -- 行数(NULLも含めて全行) COUNT(user_id) -- user_id が NULL でない行数 COUNT(DISTINCT user_id) -- user_id の異なる値の数(重複を1つに畳む)
COUNT(DISTINCT user_id) は、グループ内で重複を除いた user_id の種類数を返します。延べアクセス(COUNT(*))が同じでも、同一ユーザーの再訪が多ければ実人数(COUNT(DISTINCT user_id))は小さくなります。NULL は数えません。page_views テーブルから、日別の延べ閲覧数とユニーク訪問者数を求めてください。出力列は view_date, total_views, unique_users。view_date の昇順で返してください。
| view_id | view_date | user_id |
|---|---|---|
| 1 | 2024-05-01 | U1 |
| 2 | 2024-05-01 | U1 |
| 3 | 2024-05-01 | U2 |
| 4 | 2024-05-02 | U2 |
| 5 | 2024-05-02 | U3 |
| 6 | 2024-05-02 | U3 |
| 7 | 2024-05-02 | U1 |
| view_date | total_views | unique_users |
|---|---|---|
| 2024-05-01 | 3 | 2 |
| 2024-05-02 | 4 | 3 |
条件付き集計 — 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)
FILTER はその集計関数1つだけに条件を効かせます。だから「総数」と「paid だけの数」を同じ行に並べて出せるのです。条件に当たる行が無ければ COUNT は0、SUM は NULL になります。orders テーブルから、カテゴリ別に、総件数・ステータス別件数・paid の合計金額を1行にまとめてください。出力列は category, total_orders, paid_orders, pending_orders, cancelled_orders, paid_amount。total_orders の降順、同数なら category の昇順で返してください。
| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | paid | 1200 |
| 2 | Books | paid | 800 |
| 3 | Books | paid | 600 |
| 4 | Books | pending | 500 |
| 5 | Books | cancelled | 300 |
| 6 | Toys | paid | 2000 |
| 7 | Toys | cancelled | 1500 |
| 8 | Toys | pending | 700 |
| category | total_orders | paid_orders | pending_orders | cancelled_orders | paid_amount |
|---|---|---|---|---|---|
| Books | 5 | 3 | 1 | 1 | 2600 |
| Toys | 3 | 1 | 1 | 1 | 2000 |
小計と総計 — ROLLUP で小計・総計行を一度に出す
明細の集計に加えて「地域ごとの小計」と「全体の総計」も同じ表に出したい——そんなときは GROUP BY ROLLUP(…) です。通常のグループ化に、上位のまとめ行(小計・総計)を自動で付け足してくれます。まとめ行かどうかは GROUPING() で見分けます。
-- ROLLUP(a, b) は次の3通りの集計を一度に生成する GROUP BY ROLLUP(region, product) -- (region, product) … 明細 -- (region) … region 小計(product を畳む) -- () … 総計(すべて畳む) GROUPING(product) -- その行で product が畳まれていれば 1、明細なら 0
GROUPING(列)=1(=その列はまとめられた)で判定し、CASE で '(All)' などのラベルに置き換えるのが定番です。sales テーブルから、地域×商品の売上に、地域ごとの小計と全体の総計を加えた表を作ってください。出力列は region, product, total_amount。小計・総計の行では畳んだ列を (All) と表示し、各ブロックの末尾(明細→小計、最後に総計)に並ぶようにしてください。
| region | product | amount |
|---|---|---|
| East | Apple | 100 |
| East | Banana | 150 |
| West | Apple | 200 |
| West | Banana | 250 |
| region | product | total_amount |
|---|---|---|
| East | Apple | 100 |
| East | Banana | 150 |
| East | (All) | 250 |
| West | Apple | 200 |
| West | Banana | 250 |
| West | (All) | 450 |
| (All) | (All) | 700 |
二段階集計 — 派生テーブルで「平均の平均」を正しく出す
「顧客あたりの平均購入額」を出すつもりが、いつのまにか「注文あたりの平均」になっていた——集計の粒度を誤る典型です。ある集計の結果をさらに集計するには、サブクエリ(派生テーブル)や 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 の昇順で返してください。
| order_id | region | customer_id | amount |
|---|---|---|---|
| 1 | East | C1 | 1000 |
| 2 | East | C1 | 2000 |
| 3 | East | C2 | 3000 |
| 4 | West | C3 | 500 |
| 5 | West | C3 | 500 |
| 6 | West | C4 | 2000 |
| region | customer_count | avg_customer_spend |
|---|---|---|
| East | 2 | 3000 |
| West | 2 | 1500 |
構成比 — ウィンドウ集約で各グループの全体シェアを出す
グループ別の合計に「全体の何%か」を併記したい——ウィンドウ関数 SUM(…) OVER () を使うと、GROUP BY の結果行を保ったまま、全体の集計値を各行にブロードキャストできます。サブクエリや結合なしで構成比が1文で出せます。
-- 各グループ合計を、全グループの総計で割る SUM(amount) -- ① GROUP BY 後:このグループの合計 SUM(SUM(amount)) OVER () -- ② ①をグループ横断で合計=総計(全行に同じ値) 100.0 * SUM(amount) / SUM(SUM(amount)) OVER () -- ③ 構成比(%)
SUM(amount) は GROUP BY 後の各グループ合計。それを外側のウィンドウ SUM(…) OVER () がグループをまたいで合計し、総計を全行へ配ります。ウィンドウ関数は集計の後段で動くため、集計結果の上にもう一段の集計を重ねられるのです。OVER () は「全行が1つの窓」を意味します。sales テーブルから、カテゴリ別の売上合計と、それが全体に占める構成比(%)を求めてください。出力列は category, category_total, pct_of_total(構成比は小数第1位に四捨五入)。category_total の降順、同額なら category の昇順で返してください。
| sale_id | category | amount |
|---|---|---|
| 1 | Electronics | 4000 |
| 2 | Electronics | 2000 |
| 3 | Clothing | 1500 |
| 4 | Clothing | 500 |
| 5 | Food | 2000 |
| category | category_total | pct_of_total |
|---|---|---|
| Electronics | 6000 | 60.0 |
| Clothing | 2000 | 20.0 |
| Food | 2000 | 20.0 |