行を列に変換(ピボット) — CASE / PIVOT / crosstab
月別売上を月ごとの列に並べたいとき。行を列に変換するピボット処理。
問題 — 縦持ちを横持ちにしたい
月別売上テーブルが (month, amount) の縦持ちで入っているのを、1月 | 2月 | 3月 | ... と列に並べた横持ち(クロス集計)に変換したい、というレポート系の定番要件。これをピボットと呼びます。
注意点として、SQL は列数を動的に決められません(結果の列名・列数は SQL 本文で静的に書く必要がある)。カテゴリが 100 件ある…といった動的ピボットはアプリ側や動的 SQL 生成でやる必要があります。
解法1: CASE + 集約 — どのRDBMSでも動く
もっともポータブルで読みやすい方法。列ごとに SUM(CASE WHEN month = N THEN amount END) を書くだけ。
列が増えると SQL が長くなるのが難点だが、任意の集約関数(SUM / AVG / COUNT)が使え、RDBMS を問わず動くのが最大の強み。
SELECT
product_id,
SUM(CASE WHEN month = 1 THEN amount END) AS jan,
SUM(CASE WHEN month = 2 THEN amount END) AS feb,
SUM(CASE WHEN month = 3 THEN amount END) AS mar
FROM monthly_sales
GROUP BY product_id;解法2: SQL Server の PIVOT 演算子
SQL Server (2005+) には専用の PIVOT 構文があり、CASE の繰り返しをスッキリ書けます。Oracle 11g 以降にも類似の PIVOT があります。
便利だが、PostgreSQL / MySQL / SQLite には存在しないので、移植性が要求される環境では避けるのが無難。
-- SQL Server
SELECT product_id, [1] AS jan, [2] AS feb, [3] AS mar
FROM (
SELECT product_id, month, amount FROM monthly_sales
) src
PIVOT (
SUM(amount) FOR month IN ([1], [2], [3])
) p;解法3: PostgreSQL の crosstab (tablefunc)
PostgreSQL は tablefunc 拡張の crosstab() 関数でピボットできます。
使う前に CREATE EXTENSION tablefunc; が必要。列名・型を事前宣言する必要があって書き方がやや癖強なので、列数が多くて CASE が辛いときの選択肢と考えるとよい。通常は CASE で十分。
-- PostgreSQL (要 tablefunc 拡張)
SELECT * FROM crosstab(
'SELECT product_id, month, amount FROM monthly_sales ORDER BY 1, 2',
'SELECT generate_series(1, 3)'
) AS ct(product_id INT, jan NUMERIC, feb NUMERIC, mar NUMERIC);注意点 — 動的ピボットと NULL
カテゴリ値が動的(月次で増える、新商品が出る等)な場合、純粋な SQL での動的ピボットは不可能で、アプリ層でクエリ文字列を組み立てるか、出力を縦持ちのまま返してフロント側で整形するのが実務的。
また、対応する値がないセルは NULL になります(SUM(CASE ...) で条件に合う行がないため)。レポート表示で 0 にしたいなら COALESCE(SUM(...), 0) で包むこと。
