PIVOT は縦に並んだ行データを横方向に展開してクロス集計表を作成する SQL 構文です。Oracle 11g で追加され、月別・カテゴリ別の集計をシンプルな SQL で実現できます。CASE式による代替もありますが、PIVOT を使うとコードが簡潔になり可読性が上がります。
この記事でわかること
- PIVOT の基本構文と動作の仕組み
- SUM・COUNT・AVG・MAX を使った集計
- 複数の集計関数・複数列を同時に PIVOT する方法
- XML PIVOT で動的な列を生成する方法(Oracle 11g 以降)
- 月別売上・カテゴリ別クロス集計などの実務パターン
- CASE 式との違いと使い分け
- UNPIVOT との組み合わせ(横持ち↔縦持ちの相互変換)
PIVOT の基本構文
PIVOT を使うには対象のデータを FROM 句のインラインビューで準備し、FOR 列名 IN (値リスト) で横展開する列と値を指定します。
PIVOT の基本構文
-- 構文:
-- SELECT * FROM (インラインビュー)
-- PIVOT (
-- 集計関数(集計列)
-- FOR ピボット列 IN (値1 AS 別名1, 値2 AS 別名2, ...)
-- )
-- 例: 部門ごとの job_id 別人数をクロス集計
-- 元データ(縦持ち):
-- DEPARTMENT_ID JOB_ID COUNT
-- 50 ST_CLERK 10
-- 50 SA_REP 3
-- 90 AD_PRES 1
SELECT *
FROM (
SELECT department_id, job_id
FROM employees
)
PIVOT (
COUNT(job_id) -- 集計関数
FOR job_id IN ( -- 横展開する列
'SA_REP' AS "営業担当",
'ST_CLERK' AS "一般事務",
'AD_PRES' AS "社長"
)
)
ORDER BY department_id;
-- 結果:
-- DEPARTMENT_ID 営業担当 一般事務 社長
-- 20 0 0 0
-- 50 3 10 0
-- 90 0 0 1
SUM・AVG・MAX を使った PIVOT
-- 月別の売上合計を横展開するクロス集計(実務でよく使うパターン)
-- orders テーブル: order_date(DATE), amount(NUMBER), category(VARCHAR2)
SELECT *
FROM (
SELECT
category,
TO_CHAR(order_date, 'MM') AS month,
amount
FROM orders
WHERE order_date >= DATE '2024-01-01'
AND order_date < DATE '2025-01-01'
)
PIVOT (
SUM(amount)
FOR month IN (
'01' AS "1月", '02' AS "2月", '03' AS "3月",
'04' AS "4月", '05' AS "5月", '06' AS "6月",
'07' AS "7月", '08' AS "8月", '09' AS "9月",
'10' AS "10月", '11' AS "11月", '12' AS "12月"
)
)
ORDER BY category;
-- 結果例:
-- CATEGORY 1月 2月 3月 ... 12月
-- 家電 1200000 980000 1500000 800000
-- 衣類 450000 520000 380000 650000
複数の集計関数・複数列を同時に PIVOT する
複数集計関数を同時に PIVOT する
-- SUM と COUNT を同時に横展開する
-- → 列名は "値_集計関数" の形式で生成される(別名なしの場合)
SELECT *
FROM (
SELECT
category,
TO_CHAR(order_date, 'MM') AS month,
amount
FROM orders
WHERE order_date >= DATE '2024-01-01'
AND order_date < DATE '2025-01-01'
)
PIVOT (
SUM(amount) AS total, -- 合計
COUNT(amount) AS cnt -- 件数
FOR month IN (
'01' AS q1_jan,
'02' AS q1_feb,
'03' AS q1_mar
)
)
ORDER BY category;
-- 生成される列名: Q1_JAN_TOTAL, Q1_JAN_CNT, Q1_FEB_TOTAL, Q1_FEB_CNT, ...
-- → 集計名エイリアスと月のエイリアスが組み合わされる
XML PIVOT:値を動的に展開する(Oracle 11g 以降)
通常の PIVOT は IN リストに値を固定で列挙する必要がありますが、FOR 列 IN (ANY) を使うと列値を動的に展開できます。ただし結果は XML 型の1列になるため、アプリケーション側で解析が必要です。
XML PIVOT で列を動的に生成する
-- XML PIVOT: IN に ANY を指定すると全ての値を動的に展開(結果はXML型)
SELECT *
FROM (
SELECT department_id, job_id
FROM employees
)
PIVOT XML (
COUNT(job_id)
FOR job_id IN (ANY) -- 実際に存在する全 job_id を自動収集
)
ORDER BY department_id;
-- 結果: XML型の列 JOB_ID_XML が1つ生成される
-- <PivotSet>
-- <item><column name="JOB_ID">SA_REP</column><column name="COUNT(JOB_ID)">3</column></item>
-- ...
-- </PivotSet>
-- XML PIVOT は集計対象の値が事前にわからない場合や
-- 動的な列生成が必要なレポートツールと連携する際に使う
動的 PIVOT(列数が実行時まで決まらない場合)
本番でよく必要になる「月ごとに列数が変わる」ようなレポートは、EXECUTE IMMEDIATE で動的 SQL を組み立てる方法が最も柔軟です。SELECT DISTINCT で値リストを取得 → 文字列結合で IN リストを生成 → EXECUTE IMMEDIATE で実行します。動的 SQL の詳細はEXECUTE IMMEDIATE(動的SQL)完全ガイドを参照してください。
本番でよく必要になる「月ごとに列数が変わる」ようなレポートは、EXECUTE IMMEDIATE で動的 SQL を組み立てる方法が最も柔軟です。SELECT DISTINCT で値リストを取得 → 文字列結合で IN リストを生成 → EXECUTE IMMEDIATE で実行します。動的 SQL の詳細はEXECUTE IMMEDIATE(動的SQL)完全ガイドを参照してください。
実務パターン:employees テーブルの部門×管理職別人数
部門別・管理職フラグ別の人数クロス集計
-- manager_id がある = 部下持ち管理職と判断
-- 各部門の "管理職" / "一般" の人数を横並びで表示
SELECT *
FROM (
SELECT
d.department_name,
CASE WHEN e.employee_id IN (SELECT DISTINCT manager_id
FROM employees
WHERE manager_id IS NOT NULL)
THEN '管理職'
ELSE '一般'
END AS role
FROM employees e
JOIN departments d ON e.department_id = d.department_id
)
PIVOT (
COUNT(*)
FOR role IN (
'管理職' AS "管理職",
'一般' AS "一般社員"
)
)
ORDER BY department_name;
-- 結果例:
-- DEPARTMENT_NAME 管理職 一般社員
-- Accounting 1 1
-- Executive 1 2
-- Sales 2 32
PIVOT vs CASE 式:どちらを使うか
| 観点 | PIVOT | CASE 式 |
|---|---|---|
| コードの簡潔さ | 値リストが長くても SHORT(FOR IN のみ追記) | 値ごとに CASE WHEN … THEN … END が必要で冗長 |
| 可読性 | 横展開の意図が明確 | 集計関数がバラバラで読みにくくなることがある |
| 可搬性 | Oracle 11g 以降のみ | 標準 SQL で MySQL・PostgreSQL でも動作 |
| 動的な列 | XML PIVOT または動的 SQL が必要 | 固定 CASE 式のみ(動的は対応不可) |
| 複数集計 | 複数の集計関数を簡潔に書ける | CASE を集計関数ごとに書く必要がある |
CASE 式で同じ PIVOT を実現する(比較用)
-- PIVOT を使わずに CASE 式で月別クロス集計(Oracle 以外でも動作する書き方)
SELECT
category,
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '01' THEN amount END) AS "1月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '02' THEN amount END) AS "2月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '03' THEN amount END) AS "3月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '04' THEN amount END) AS "4月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '05' THEN amount END) AS "5月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '06' THEN amount END) AS "6月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '07' THEN amount END) AS "7月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '08' THEN amount END) AS "8月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '09' THEN amount END) AS "9月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '10' THEN amount END) AS "10月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '11' THEN amount END) AS "11月",
SUM(CASE WHEN TO_CHAR(order_date, 'MM') = '12' THEN amount END) AS "12月"
FROM orders
WHERE order_date >= DATE '2024-01-01'
AND order_date < DATE '2025-01-01'
GROUP BY category
ORDER BY category;
-- → PIVOT 版より冗長だが、MySQL・PostgreSQL・SQL Server でも動作する
まとめ
- PIVOT の基本:インラインビューで3列(グループ列・展開列・集計列)を用意 →
FOR 列 IN (値リスト)で横展開 - 複数集計:集計関数を複数書くと “値_集計名” の列が自動生成される
- XML PIVOT:
IN (ANY)で動的展開(結果は XML 型。完全な動的 PIVOT は EXECUTE IMMEDIATE を使う) - CASE 式との使い分け:Oracle 専用でよければ PIVOT が簡潔。DB 移植性が必要なら CASE 式
- UNPIVOT:横持ち → 縦持ちへの逆変換。PIVOT と組み合わせてデータを整形できる
横持ち → 縦持ちへの変換はUNPIVOTの使い方完全ガイドを、CASE 式を使ったクロス集計・条件集計はCASE式完全ガイドも参照してください。

