【Oracle】PIVOTの使い方完全ガイド|縦持ちを横持ちに変換・月別クロス集計・複数集計・XML PIVOT・CASE式との比較

【Oracle】PIVOTの使い方完全ガイド|縦持ちを横持ちに変換・月別クロス集計・複数集計・XML PIVOT・CASE式との比較 Oracle

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)完全ガイドを参照してください。

実務パターン: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 PIVOTIN (ANY) で動的展開(結果は XML 型。完全な動的 PIVOT は EXECUTE IMMEDIATE を使う)
  • CASE 式との使い分け:Oracle 専用でよければ PIVOT が簡潔。DB 移植性が必要なら CASE 式
  • UNPIVOT:横持ち → 縦持ちへの逆変換。PIVOT と組み合わせてデータを整形できる

横持ち → 縦持ちへの変換はUNPIVOTの使い方完全ガイドを、CASE 式を使ったクロス集計・条件集計はCASE式完全ガイドも参照してください。