条件付き集計でピボット — FILTER で行を列に展開しクロス集計表を作る
基礎編で学んだ FILTER を応用すると、縦持ち(long)のデータを横持ち(wide)のクロス集計表へ展開(ピボット)できます。ステータスごとの集計を別々の列として並べ、1カテゴリ=1行のサマリ表を作ります。
-- status の値ごとに「列」を作る(ピボット) SUM(amount) FILTER (WHERE status = 'completed') -- 完了の列 SUM(amount) FILTER (WHERE status = 'pending') -- 保留の列 COALESCE(SUM(...) FILTER (...), 0) -- 該当なしの NULL を 0 に
SUM(...) FILTER(...) は 0 ではなく NULL を返します。クロス集計表では空欄を 0 と見せたいことが多いため、COALESCE(集計, 0) で 0 に変換します。CASE 式版 SUM(CASE WHEN status='completed' THEN amount ELSE 0 END) でも同じ表を作れます。orders から、カテゴリを行・ステータスを列にしたピボット表を作ってください。出力列は category, completed_amt, pending_amt, cancelled_amt, total_amt。該当が無いセルは 0 とし、total_amt の降順で返してください。
| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | completed | 1200 |
| 2 | Books | completed | 800 |
| 3 | Books | cancelled | 500 |
| 4 | Toys | completed | 3000 |
| 5 | Toys | pending | 1500 |
| 6 | Toys | cancelled | 1000 |
| 7 | Food | completed | 600 |
| 8 | Food | pending | 400 |
| category | completed_amt | pending_amt | cancelled_amt | total_amt |
|---|---|---|---|---|
| Toys | 3000 | 1500 | 1000 | 5500 |
| Books | 2000 | 0 | 500 | 2500 |
| Food | 600 | 400 | 0 | 1000 |
SELECT category, COALESCE(SUM(amount) FILTER (WHERE status = 'completed'), 0) AS completed_amt, -- 完了の合計(該当なしは0に) COALESCE(SUM(amount) FILTER (WHERE status = 'pending'), 0) AS pending_amt, -- 保留の合計(該当なしは0に) COALESCE(SUM(amount) FILTER (WHERE status = 'cancelled'), 0) AS cancelled_amt, -- 取消の合計(該当なしは0に) SUM(amount) AS total_amt -- 全ステータス合計(FILTERなし=グループ全体) FROM orders GROUP BY category -- 行の軸=カテゴリでグループ化 ORDER BY total_amt DESC; -- 合計の降順で並べ替え /* 実行順序(SQLの論理的な評価順): 1. FROM orders → 行を読み込む 2. GROUP BY category → グループ化 3. FILTER 付き集計 → 集計関数を評価 4. SELECT → 列を評価(COALESCE で整形) 5. ORDER BY total_amt DESC → 並び替えて出力 */
LEGEND
① FROM orders(8行)
FROM ordersorders テーブルの8行を読み込みます。status には completed / pending / cancelled の3種があり、これを「列」に展開していきます。| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | completed | 1200 |
| 2 | Books | completed | 800 |
| 3 | Books | cancelled | 500 |
| 4 | Toys | completed | 3000 |
| 5 | Toys | pending | 1500 |
| 6 | Toys | cancelled | 1000 |
| 7 | Food | completed | 600 |
| 8 | Food | pending | 400 |
SUM(amount) FILTER (WHERE status='...') を「列」として並べると、1カテゴリ=1行のクロス集計表になります。1回のグループ化・1回のスキャンで複数列を同時に生成でき、明細を何度も読み直す必要がありません。SUM(...) FILTER(...) は 0 ではなく NULL を返します。表の見栄えと後続計算のために COALESCE(集計, 0) で 0 に整えます。COUNT は0、SUM/AVG/MAX は NULL を返す違いを押さえましょう。SUM(x) FILTER (WHERE c) は SUM(CASE WHEN c THEN x END) と同じ結果です。PostgreSQL では FILTER が読みやすい第一選択、他の DB では CASE 版で移植します。どちらも「条件で列を切り分ける」ピボットの心臓部です。GROUP BY category, status にすると元の縦持ちに逆戻りします。ピボットでは行の軸(category)だけを GROUP BY し、列の軸(status)は FILTER の条件側で展開するのが正解です。DATE_TRUNC で時系列グループ化 — 日付を月に丸めて月次集計する
時系列データの集計では、日付を月や週などの単位に切り捨ててグループ化します。PostgreSQL の DATE_TRUNC('month', ts) は、タイムスタンプをその月の初日 00:00に丸めるため、同じ月の行が自然に1グループへまとまります。
-- 月初に切り捨てて「月」でグループ化 DATE_TRUNC('month', order_date) -- 2024-02-15 → 2024-02-01 GROUP BY DATE_TRUNC('month', order_date)
::date で時刻を落とすと読みやすくなります。orders から、月別の注文件数と合計金額を求めてください。出力列は month, order_count, total_amount。month は月初日(date 型)で表し、month の昇順で返してください。
| order_id | order_date | amount |
|---|---|---|
| 1 | 2024-01-05 | 1000 |
| 2 | 2024-01-20 | 1500 |
| 3 | 2024-02-03 | 2000 |
| 4 | 2024-02-15 | 500 |
| 5 | 2024-02-28 | 1000 |
| 6 | 2024-03-10 | 3000 |
| 7 | 2024-03-22 | 2000 |
| month | order_count | total_amount |
|---|---|---|
| 2024-01-01 | 2 | 2500 |
| 2024-02-01 | 3 | 3500 |
| 2024-03-01 | 2 | 5000 |
SELECT DATE_TRUNC('month', order_date)::date AS month, -- 月初に切り捨て、::date で時刻を除去して表示を整える COUNT(*) AS order_count, -- 月内の注文件数 SUM(amount) AS total_amount -- 月内の合計金額 FROM orders GROUP BY DATE_TRUNC('month', order_date) -- 切り捨てた「月」単位でグループ化(式をそのまま書く) ORDER BY month; -- 月初日の昇順=時系列順に並べ替え /* 実行順序(SQLの論理的な評価順): 1. FROM orders → 行を読み込む 2. DATE_TRUNC → 値を整形(月初に変換) 3. GROUP BY (月) → グループ化 4. COUNT / SUM → 集計関数を評価 5. ORDER BY month → 並び替えて出力 */
LEGEND
① FROM orders(7行)
FROM orders7行を読み込みます。order_date は日単位でバラバラですが、これを月の粒度に丸めて集計するのが目的です。| order_id | order_date | amount |
|---|---|---|
| 1 | 2024-01-05 | 1000 |
| 2 | 2024-01-20 | 1500 |
| 3 | 2024-02-03 | 2000 |
| 4 | 2024-02-15 | 500 |
| 5 | 2024-02-28 | 1000 |
| 6 | 2024-03-10 | 3000 |
| 7 | 2024-03-22 | 2000 |
DATE_TRUNC('month', ts) は日付を月初に丸め、同じ月の行を1グループに束ねます。'day' / 'week' / 'month' / 'quarter' / 'year' と粒度を変えるだけで、同じデータから日次・月次・年次レポートを自在に切り替えられます。DATE_TRUNC(...))なら、GROUP BY にも同じ式を書きます。PostgreSQL は出力別名や序数(GROUP BY 1)も許しますが、式を明示する方が他 DBへ移植しやすく意図も明確です。DATE_TRUNC の戻り値は時刻付きの timestamp です。::date で時刻を落とす、TO_CHAR(month,'YYYY-MM') で 'YYYY-MM' の文字列にするなど、集計のキーと表示の整形は分けて考えると扱いやすくなります。GROUP BY order_date は1日(タイムスタンプ単位なら1秒)ごとに分割され、月次のつもりが大量の行に膨らみます。集計したい粒度に必ず DATE_TRUNC で丸めてからグループ化します。GROUP BY EXTRACT(MONTH FROM order_date) は「月の数字」だけで束ねるため、2024年1月と2025年1月が同じグループに混ざります。年も区別したいなら DATE_TRUNC か (年, 月) の組をキーにします。generate_series('2024-01-01','2024-03-01','1 month') で連続した日付軸を作り、集計結果を LEFT JOIN して COALESCE(..., 0) で穴埋めするのが定石です。「データに現れた行しか出ない」という GROUP BY の性質を、時系列では特に意識してください。ROLLUP で小計と総計 — 1クエリで明細・カテゴリ小計・全体総計を出す
合計行を伴うレポートは GROUP BY ROLLUP が最短です。ROLLUP(a, b) は (a,b) の明細に加え、(a) の小計と () の総計を、1回のクエリでまとめて出力します。
-- 明細 + カテゴリ小計 + 総計を一度に生成 GROUP BY ROLLUP (category, status) -- 生成される集計単位: (cat,status) / (cat) / () GROUPING(status) -- 1 なら status を畳んだ小計/総計行
GROUPING(列)(小計なら1)を使い、ラベル付けや並べ替えに利用します。orders から、カテゴリ×ステータスの明細に加え、カテゴリ小計と全体総計を1つのクエリで出してください。小計行の status は「(小計)」、総計行は category「【全カテゴリ】」・status「(総計)」と表示し、レポート順(カテゴリ→明細→小計→最後に総計)で返してください。
| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | completed | 1200 |
| 2 | Books | cancelled | 800 |
| 3 | Toys | completed | 3000 |
| 4 | Toys | completed | 1000 |
| 5 | Toys | cancelled | 500 |
| 6 | Food | completed | 600 |
| category | status | total_amount |
|---|---|---|
| Books | cancelled | 800 |
| Books | completed | 1200 |
| Books | (小計) | 2000 |
| Food | completed | 600 |
| Food | (小計) | 600 |
| Toys | cancelled | 500 |
| Toys | completed | 4000 |
| Toys | (小計) | 4500 |
| 【全カテゴリ】 | (総計) | 7100 |
SELECT COALESCE(category, '【全カテゴリ】') AS category, -- 総計行は category が NULL → ラベルに置換 COALESCE( status, CASE WHEN GROUPING(category) = 0 THEN '(小計)' -- category は生きていて status だけ畳まれた=小計 ELSE '(総計)' END -- 両方畳まれた=総計 ) AS status, SUM(amount) AS total_amount -- 各集計単位(明細/小計/総計)の合計 FROM orders GROUP BY ROLLUP (category, status) -- (cat,status)/(cat)/() の3階層を一度に生成 ORDER BY GROUPING(category), category, -- 総計(GROUPING=1)を最後へ/カテゴリ昇順 GROUPING(status), status; -- 小計(GROUPING=1)を各カテゴリの最後へ /* 実行順序(SQLの論理的な評価順): 1. FROM orders → 行を読み込む 2. GROUP BY ROLLUP(category, status) → グループ化(明細・小計・総計) 3. SUM(amount) → 集計関数を評価 4. SELECT → 列を評価(COALESCE + GROUPING で整形) 5. ORDER BY GROUPING(category), category, GROUPING(status), status → 並び替えて出力 */
LEGEND
① FROM orders(6行)
FROM orders6行を読み込みます。category(3種)と status(completed/cancelled)の組み合わせ単位に加え、その小計と総計まで一気に作るのがゴールです。| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | completed | 1200 |
| 2 | Books | cancelled | 800 |
| 3 | Toys | completed | 3000 |
| 4 | Toys | completed | 1000 |
| 5 | Toys | cancelled | 500 |
| 6 | Food | completed | 600 |
ROLLUP(a, b) は (a,b)→(a)→() と次元を右から1つずつ畳み、明細・小計・総計をまとめて出力します。UNION ALL で何本も書く必要が無く、テーブルのスキャンも1回で済みます。COALESCE で「(小計)/(総計)」のラベルに置き換えると、そのままレポートとして読めます。GROUPING(列)=1 ならその列が畳まれた行です。ORDER BY GROUPING(category), category, GROUPING(status), status とすると、明細→小計→総計のレポート順が安定して得られ、本物の NULL データとも確実に区別できます。status=NULL を「データ抜け」と読むと、小計を明細と二重に数えてしまいます。畳まれた NULL か実データの NULL かは GROUPING() で必ず判定してください。GROUP BY cat,status ∪ GROUP BY cat ∪ 全体、と3本のクエリを継ぎ足すのは冗長で、テーブルを3回走査します。ROLLUP なら1回のスキャンで同じ結果が得られます。構成比と累積構成比 — GROUP BY とウィンドウ関数で「全体に占める割合」
「各カテゴリは全体の何%か」を出すには、グループ集計の結果に対してさらにウィンドウ関数を重ねます。SUM(SUM(amount)) OVER () は、内側 SUM がグループ合計、外側 SUM が全グループを横断した総計を表す、集計の二段重ねです。
-- 構成比 = グループ合計 / 全体総計 SUM(amount) * 100.0 / SUM(SUM(amount)) OVER () -- 累積構成比(合計の大きい順に積み上げ) SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC)
SUM(amount))を参照でき、SUM(SUM(...)) という二重集計が成立します。OVER () は全行を1つの窓に、OVER (ORDER BY ...) は累積(ランニング合計)を作ります。orders から、カテゴリ別の合計金額・全体に占める構成比(%)・累積構成比(%)を求めてください。出力列は category, total_amount, pct_of_total, cumulative_pct。合計の降順(ABC分析の並び)で、割合は小数第1位まで丸めて返してください。
| order_id | category | amount |
|---|---|---|
| 1 | Books | 1000 |
| 2 | Books | 1000 |
| 3 | Toys | 3000 |
| 4 | Toys | 2000 |
| 5 | Food | 2000 |
| 6 | Food | 1000 |
| category | total_amount | pct_of_total | cumulative_pct |
|---|---|---|---|
| Toys | 5000 | 50.0 | 50.0 |
| Food | 3000 | 30.0 | 80.0 |
| Books | 2000 | 20.0 | 100.0 |
SELECT category, SUM(amount) AS total_amount, -- カテゴリ別の合計(内側の集計) ROUND( SUM(amount) * 100.0 / SUM(SUM(amount)) OVER (), 1 -- 自グループ合計 ÷ 全体総計(窓=全カテゴリ) ) AS pct_of_total, -- 構成比(%) ROUND( SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC) * 100.0 -- 合計の大きい順に積み上げた累積 / SUM(SUM(amount)) OVER (), 1 ) AS cumulative_pct -- 累積構成比(ABC分析) FROM orders GROUP BY category -- まずカテゴリで集計 ORDER BY total_amount DESC; -- 合計の降順(累積の積み上げ順と一致させる) /* 実行順序(SQLの論理的な評価順): 1. FROM orders → 行を読み込む 2. GROUP BY category → グループ化 3. SUM(amount) → 集計関数を評価 4. ウィンドウ関数 → ウィンドウ関数を評価(行数は保持) 5. ORDER BY total_amount DESC → 並び替えて出力 */
LEGEND
① FROM orders(6行)
FROM orders6行を読み込みます。まずカテゴリ別に金額を合計し、その後で「全体に対する割合」を計算します。| order_id | category | amount |
|---|---|---|
| 1 | Books | 1000 |
| 2 | Books | 1000 |
| 3 | Toys | 3000 |
| 4 | Toys | 2000 |
| 5 | Food | 2000 |
| 6 | Food | 1000 |
SUM(amount) を参照できます。OVER () は全体を1つの窓に、OVER (ORDER BY ...) は累積(ランニング合計)を作る、と覚えておくと応用が利きます。cumulative_pct を見れば、「上位何カテゴリで全体の8割を占めるか」が一目で分かります。在庫・売上・顧客の重点管理(パレートの法則)で頻出の表現です。SUM(amount) / SUM(SUM(amount)) OVER () を整数のまま割ると、商が1未満のため すべて 0 になります。* 100.0 を掛ける、または ::numeric で実数化してから割ってください。WHERE SUM(...) OVER () > ... は書けません。構成比で絞り込みたいなら、いったんサブクエリ/CTE で包んでから外側の WHERE で絞ります。SUM(...) OVER () を1行足すだけで「全体に対する割合」が付き、これを OVER (PARTITION BY 月) に変えれば「その月の中でのシェア」に早変わりします。同じ二段集計の発想で、月次×カテゴリの中での構成比、地域内シェアなど切り口を自在に変えられます。総計をスカラサブクエリ (SELECT SUM(amount) FROM orders) で別取りしてもよいですが、ウィンドウ版はテーブルを1回しか読まず、読みやすさでも勝ります。STRING_AGG / ARRAY_AGG — グループ内の値を1つのリストに集約する
グループ内の値を1つのリストにまとめたいときは STRING_AGG(文字列連結)や ARRAY_AGG(配列化)を使います。集計関数の内側に ORDER BY や DISTINCT を書けるのが特徴です。
-- グループ内の値をカンマ区切りで連結(重複除去・並べ替え付き) STRING_AGG(DISTINCT product, ', ' ORDER BY product) -- 配列にまとめる場合 ARRAY_AGG(DISTINCT product ORDER BY product)
STRING_AGG(expr, 区切り ORDER BY ...) の ORDER BY はリスト要素の並び順を決めます(クエリ末尾の ORDER BY とは別物)。DISTINCT を付けると重複要素を1つにまとめます(このとき ORDER BY 式は DISTINCT 対象と一致させる必要があります)。sales から、カテゴリ別に 取扱商品数(種類)・商品名リスト・合計数量を求めてください。商品名リストは重複を除き名前順、出力列は category, product_count, products, total_qty。2種類以上の商品を扱うカテゴリだけを、total_qty の降順で返してください。
| id | category | product | qty |
|---|---|---|---|
| 1 | Books | SQL Guide | 3 |
| 2 | Books | Python | 1 |
| 3 | Books | SQL Guide | 2 |
| 4 | Toys | Blocks | 5 |
| 5 | Toys | Puzzle | 2 |
| 6 | Toys | Blocks | 1 |
| 7 | Food | Coffee | 4 |
| category | product_count | products | total_qty |
|---|---|---|---|
| Toys | 2 | Blocks, Puzzle | 8 |
| Books | 2 | Python, SQL Guide | 6 |
SELECT category, COUNT(DISTINCT product) AS product_count, -- 重複を除いた商品の種類数 STRING_AGG(DISTINCT product, ', ' ORDER BY product) AS products, -- 商品名を名前順・重複なしで連結 SUM(qty) AS total_qty -- 数量の合計 FROM sales GROUP BY category -- カテゴリ単位でグループ化 HAVING COUNT(DISTINCT product) >= 2 -- 商品が2種類以上のグループだけ残す(集計後フィルタ) ORDER BY total_qty DESC; -- 合計数量の降順で並べ替え /* 実行順序(SQLの論理的な評価順): 1. FROM sales → 行を読み込む 2. GROUP BY category → グループ化 3. 集計 → 集計関数を評価 4. HAVING → グループを絞り込む 5. ORDER BY total_qty DESC → 並び替えて出力 */
LEGEND
① FROM sales(7行)
FROM sales7行を読み込みます。product には重複があり(Books の SQL Guide が2回、Toys の Blocks が2回)、これを種類としてまとめます。| id | category | product | qty |
|---|---|---|---|
| 1 | Books | SQL Guide | 3 |
| 2 | Books | Python | 1 |
| 3 | Books | SQL Guide | 2 |
| 4 | Toys | Blocks | 5 |
| 5 | Toys | Puzzle | 2 |
| 6 | Toys | Blocks | 1 |
| 7 | Food | Coffee | 4 |
STRING_AGG(DISTINCT x, ',' ORDER BY x) の ORDER BY はリスト内の並び順、DISTINCT は重複要素の除去を担います。クエリ末尾の ORDER BY(行の並び順)とは役割が異なる点に注意してください。HAVING COUNT(DISTINCT product) >= 2 は集計後の判定なので HAVING に書きます。WHERE の段階では COUNT(DISTINCT ...) はまだ計算されておらず参照できません。基礎編の WHERE/HAVING の使い分けが、リスト集約でもそのまま効きます。"SQL Guide, Python, SQL Guide" のように重複がそのまま連結されます。一覧の重複除去は、クエリ末尾ではなく集計関数の内側の DISTINCT で行います。ORDER BY は「行の順序」を変えるだけで、STRING_AGG が作るリスト内部の並びは変わりません。要素順は必ず集計関数の内側の ORDER BY で指定します。STRING_AGG(... ORDER BY ...) に件数上限の発想(上位 N 件だけ連結する、LEFT(string, n) || '...' で切り詰める)を併用し、全件が必要なら配列を返す ARRAY_AGG+アプリ側整形に切り替えるなど、出力サイズを意識した設計が安全です。