【完全ガイド】Oracle PIVOT / UNPIVOT 徹底解説|クロス集計・動的PIVOT・実践パターンまで

【完全ガイド】Oracle PIVOT / UNPIVOT 徹底解説|クロス集計・動的PIVOT・実践パターンまで

Oracle SQL でレポート作成に必須のスキルPIVOTUNPIVOT:

-- 部署 × 職種のクロス集計
SELECT * FROM (
  SELECT department_id, job_id, salary FROM employees
)
PIVOT (
  SUM(salary)
  FOR job_id IN ('IT_PROG' AS it, 'HR_REP' AS hr, 'SA_REP' AS sales)
);

結果:

DEPARTMENT_ID  IT     HR    SALES
           10  0      6500  0
           20  6000   0     0
           30  4200   0     14000

1つの SQL で行列変換。GROUP BY + CASE で書くと非常に冗長になるものが、シンプルに書けます。

Oracle 11g で導入されたモダンな SQL 機能ですが、日本語での実践的解説が少なく:

  • CASE + GROUP BY で複雑に書いてしまう
  • PIVOT の IN 句のエイリアスを知らない
  • UNPIVOT の存在を知らない
  • INCLUDE NULLS / EXCLUDE NULLSの使い分けが分からない
  • 動的 PIVOT(列が動的に変わる)を諦める
  • XML PIVOTの存在を知らない
  • PIVOT-UNPIVOT を連携した ETL パターンを知らない
  • Rails / BI ツールでの活用が分からない

さらに、実務では:

  • KPI レポートでクロス集計必須
  • 月次/四半期レポート
  • BI ダッシュボードの SQL 側実装
  • ETL パイプラインの正規化・非正規化
  • サーベイデータの分析
  • エクセルライクな出力
  • 他 DB データの統合(構造変換)

など、モダン Oracle 開発の必須スキルです。

本記事では、Oracle PIVOT / UNPIVOT完全ガイドを、リファレンスとして実用的に整理します。CASE + GROUP BY との比較、PIVOT/UNPIVOT の詳細構文、動的 PIVOT(EXECUTE IMMEDIATE、XML 型)、10大実践パターン、分析関数との連携、Rails/Java/Python 対応、実践シナリオ、パフォーマンス、FAQまで完全網羅。この1本で Oracle PIVOT/UNPIVOT を根本から使いこなせるようになります。


目次

結論:行↔列の変換

時間がない方向けに、最速の理解を示します。

PIVOT vs UNPIVOT

項目PIVOTUNPIVOT
方向行 → 列(横持ち)列 → 行(縦持ち)
集約必須(SUM, COUNT等)不要
用途レポート、ダッシュボード正規化、ETL
NULL自動的に処理INCLUDE/EXCLUDE NULLS
動的静的のみ(要 dynamic SQL)静的のみ

基本構文

-- PIVOT
SELECT * FROM (<ソースクエリ>)
PIVOT (
  <集約関数>(<列>)
  FOR <ピボット列> IN (<値1>, <値2>, ...)
);

-- UNPIVOT
SELECT * FROM <テーブル>
UNPIVOT [INCLUDE NULLS | EXCLUDE NULLS] (
  <値列名>
  FOR <カテゴリ列名> IN (<列1>, <列2>, ...)
);

5大パターン

-- ① 基本 PIVOT
PIVOT (SUM(sales) FOR month IN ('Jan', 'Feb', 'Mar'))

-- ② エイリアス付き
PIVOT (SUM(sales) FOR month IN ('Jan' AS jan_sales, 'Feb' AS feb_sales))

-- ③ 複数集約
PIVOT (SUM(sales) AS total, COUNT(*) AS cnt FOR month IN ('Jan', 'Feb'))

-- ④ 複数ピボット列
PIVOT (SUM(sales) FOR (year, quarter) IN ((2025, 1), (2025, 2)))

-- ⑤ UNPIVOT
UNPIVOT (sales FOR month IN (jan, feb, mar))

詳細は以下で解説します。


まず理解する:PIVOT の本質

縦持ちデータ

DEPT   PRODUCT   AMOUNT
10     A         100
10     B         200
20     A         150
20     B         250

横持ちデータ(PIVOT 後)

DEPT   A     B
10     100   200
20     150   250

同じデータを別の視点で表示。レポートで見やすい形式。

従来の CASE + GROUP BY

SELECT dept,
       SUM(CASE WHEN product = 'A' THEN amount ELSE 0 END) AS a,
       SUM(CASE WHEN product = 'B' THEN amount ELSE 0 END) AS b,
       SUM(CASE WHEN product = 'C' THEN amount ELSE 0 END) AS c
FROM sales
GROUP BY dept;

冗長、列が増えると管理困難。

PIVOT を使えば

SELECT * FROM (SELECT dept, product, amount FROM sales)
PIVOT (SUM(amount) FOR product IN ('A' AS a, 'B' AS b, 'C' AS c));

シンプル、可読性高い。


PIVOT の詳細構文

基本形

SELECT * FROM (
  SELECT <暗黙GROUP BY列>, <ピボット列>, <集約列>
  FROM <ソース>
)
PIVOT (
  <集約関数>(<集約列>)
  FOR <ピボット列> IN (<値1> [AS <別名1>], <値2> [AS <別名2>], ...)
);

重要ポイント

暗黙の GROUP BY:

  • サブクエリで指定していない列が自動的に GROUP BY 対象
  • 必要な列だけをサブクエリで選択する
-- ❌ 全列を SELECT すると意図しない GROUP BY
SELECT * FROM (SELECT * FROM sales)
PIVOT (SUM(amount) FOR product IN ('A', 'B'));
-- sales の全列で GROUP BY

-- ✅ 必要な列だけ
SELECT * FROM (SELECT dept, product, amount FROM sales)
PIVOT (SUM(amount) FOR product IN ('A', 'B'));

エイリアス

PIVOT (
  SUM(amount) 
  FOR product IN ('A' AS product_a, 'B' AS product_b)
)

別名で列名を制御、識別子として使える。

数値のエイリアス

PIVOT (
  COUNT(*)
  FOR dept_id IN (10 AS dept_10, 20 AS dept_20, 30 AS dept_30)
)

数値も IN 句に指定可能

複数集約

SELECT * FROM (
  SELECT dept, product, amount FROM sales
)
PIVOT (
  SUM(amount) AS total,
  COUNT(*) AS cnt,
  AVG(amount) AS avg_amt
  FOR product IN ('A' AS a, 'B' AS b)
);

結果:

DEPT  A_TOTAL  A_CNT  A_AVG_AMT  B_TOTAL  B_CNT  B_AVG_AMT

列数 = 集約数 × ピボット値数

複数ピボット列

SELECT * FROM (
  SELECT dept, year, quarter, amount FROM sales
)
PIVOT (
  SUM(amount)
  FOR (year, quarter) IN (
    (2025, 1) AS q1_2025,
    (2025, 2) AS q2_2025,
    (2025, 3) AS q3_2025,
    (2025, 4) AS q4_2025
  )
);

タプルで複数列を組み合わせ

PIVOT XML(動的対応)

SELECT * FROM (
  SELECT dept, product, amount FROM sales
)
PIVOT XML (
  SUM(amount)
  FOR product IN (SELECT DISTINCT product FROM sales)
);

IN 句にサブクエリ可能。結果は XML 型。


UNPIVOT の詳細構文

基本形

SELECT * FROM <テーブル>
UNPIVOT [INCLUDE NULLS | EXCLUDE NULLS] (
  <値列名>
  FOR <カテゴリ列名> IN (
    <列名1> [AS <ラベル1>],
    <列名2> [AS <ラベル2>],
    ...
  )
);

横持ちテーブル:

DEPT  Q1   Q2   Q3   Q4
10    100  200  150  300
20    250  180  220  400

UNPIVOT:

SELECT * FROM quarterly_sales
UNPIVOT (
  amount
  FOR quarter IN (q1 AS 'Q1', q2 AS 'Q2', q3 AS 'Q3', q4 AS 'Q4')
);

結果:

DEPT  QUARTER  AMOUNT
10    Q1       100
10    Q2       200
10    Q3       150
10    Q4       300
20    Q1       250
...

列 → 行に変換、正規化。

INCLUDE NULLS vs EXCLUDE NULLS

EXCLUDE NULLS(デフォルト):

UNPIVOT EXCLUDE NULLS (
  amount FOR quarter IN (q1, q2, q3, q4)
)
-- NULL の行を除外

INCLUDE NULLS:

UNPIVOT INCLUDE NULLS (
  amount FOR quarter IN (q1, q2, q3, q4)
)
-- NULL の行も含める

BI ツールやレポートで NULL の扱いが重要

複数値の UNPIVOT

-- 売上と数量の両方
SELECT * FROM monthly_sales
UNPIVOT (
  (amount, quantity)
  FOR month IN (
    (jan_amt, jan_qty) AS 'Jan',
    (feb_amt, feb_qty) AS 'Feb'
  )
);

タプルで複数列を同時に縦持ち化。


動的 PIVOT の実装

課題

Oracle PIVOT は静的(IN 句のリストがコンパイル時に決まる):

-- ❌ できない
PIVOT (SUM(amount) FOR product IN (SELECT DISTINCT product FROM sales))
-- Oracle 11g/12c/19c/23ai すべて未対応(通常構文)

新しい商品が追加されると SQL 書き直し。

解決策A: EXECUTE IMMEDIATE

DECLARE
  v_columns VARCHAR2(4000);
  v_sql     VARCHAR2(4000);
BEGIN
  -- IN 句を動的に構築
  SELECT LISTAGG('''' || product || ''' AS ' || product, ', ') 
         WITHIN GROUP (ORDER BY product)
  INTO v_columns
  FROM (SELECT DISTINCT product FROM sales);
  
  -- 動的 SQL
  v_sql := 'SELECT * FROM (SELECT dept, product, amount FROM sales) 
            PIVOT (SUM(amount) FOR product IN (' || v_columns || '))';
  
  -- 実行
  EXECUTE IMMEDIATE v_sql;
END;
/

解決策B: PIVOT XML

SELECT * FROM (
  SELECT dept, product, amount FROM sales
)
PIVOT XML (
  SUM(amount)
  FOR product IN (SELECT DISTINCT product FROM sales)
);

結果は XML 型、パースが必要:

SELECT dept,
       EXTRACT(product_xml, '/PivotSet/item[column="A"]/column[@name="MAX(AMT)"]').getStringVal() AS product_a
FROM (
  SELECT dept, product_xml FROM sales
  PIVOT XML (SUM(amount) FOR product IN (SELECT DISTINCT product FROM sales)) 
);

扱いにくい、BI ツール側でパースが現実的。

解決策C: レポート層で対応

BI ツール(Tableau, Power BI)や
アプリ層(Rails, Java)で PIVOT
→ SQL は正規形(縦持ち)

最もクリーン、多くの場合はこれ。

解決策D: LISTAGG で PIVOT 相当

SELECT dept,
       LISTAGG(product || ':' || amount, ', ') 
         WITHIN GROUP (ORDER BY product) AS products
FROM sales
GROUP BY dept;

文字列連結でクロス集計相当。


実践パターン 10選

パターン① 月次売上クロス集計

SELECT * FROM (
  SELECT customer_id, TO_CHAR(order_date, 'MM') month, amount
  FROM orders
  WHERE EXTRACT(YEAR FROM order_date) = 2026
)
PIVOT (
  SUM(amount)
  FOR month IN (
    '01' AS jan, '02' AS feb, '03' AS mar, '04' AS apr,
    '05' AS may, '06' AS jun, '07' AS jul, '08' AS aug,
    '09' AS sep, '10' AS oct, '11' AS nov, '12' AS dec
  )
);

パターン② 部署別×職種別ヘッドカウント

SELECT * FROM (
  SELECT department_id, job_id FROM employees
)
PIVOT (
  COUNT(*)
  FOR job_id IN (
    'IT_PROG' AS it,
    'HR_REP' AS hr,
    'SA_REP' AS sales,
    'FI_ACCOUNT' AS finance
  )
);

パターン③ NULL を 0 に変換

SELECT department_id,
       NVL(it, 0) AS it,
       NVL(hr, 0) AS hr,
       NVL(sales, 0) AS sales
FROM (
  SELECT department_id, job_id FROM employees
)
PIVOT (
  COUNT(*)
  FOR job_id IN ('IT_PROG' AS it, 'HR_REP' AS hr, 'SA_REP' AS sales)
);

未参加カテゴリを 0 に。ゼロ除算注意は ORA-01476: divisor is equal to zero の記事も参照してください。

パターン④ 複数集計値

SELECT * FROM (
  SELECT department_id, job_id, salary FROM employees
)
PIVOT (
  COUNT(*) AS count,
  SUM(salary) AS total,
  AVG(salary) AS avg,
  MAX(salary) AS max
  FOR job_id IN ('IT_PROG' AS it, 'HR_REP' AS hr)
);

列数 = 集約数 × 値数(8列生成)。

パターン⑤ PIVOT-UNPIVOT 連携(正規化)

-- CSV のような横持ちを縦持ちに
SELECT store_id, product, quantity FROM store_inventory
UNPIVOT (
  quantity FOR product IN (
    apples AS 'Apples',
    oranges AS 'Oranges',
    bananas AS 'Bananas'
  )
);

正規化 ETLの第一歩。

パターン⑥ 年別トレンド

SELECT * FROM (
  SELECT product_id, 
         EXTRACT(YEAR FROM sale_date) year,
         amount
  FROM sales
)
PIVOT (
  SUM(amount)
  FOR year IN (2023, 2024, 2025, 2026)
)
ORDER BY product_id;

パターン⑦ Cohort 分析(週×コホート)

SELECT * FROM (
  SELECT cohort_month,
         week_since_join,
         active_users
  FROM cohort_data
)
PIVOT (
  SUM(active_users)
  FOR week_since_join IN (
    0 AS w0, 1 AS w1, 2 AS w2, 4 AS w4, 
    8 AS w8, 12 AS w12, 24 AS w24
  )
)
ORDER BY cohort_month;

分析関数関連は Oracle 分析関数(OVER/PARTITION BY)の記事も参照してください。

パターン⑧ 生存表示(Yes/No)

SELECT customer_id,
       CASE WHEN feb IS NULL THEN 'N' ELSE 'Y' END AS active_feb,
       CASE WHEN mar IS NULL THEN 'N' ELSE 'Y' END AS active_mar
FROM (
  SELECT customer_id, TO_CHAR(login_date, 'MM') month
  FROM logins
)
PIVOT (
  COUNT(*)
  FOR month IN ('01' AS jan, '02' AS feb, '03' AS mar)
);

パターン⑨ 分析関数と併用

-- PIVOT した後、行内で分析
WITH pivoted AS (
  SELECT * FROM (
    SELECT customer_id, TO_CHAR(order_date, 'MM') month, amount
    FROM orders
  )
  PIVOT (
    SUM(amount)
    FOR month IN ('01' AS jan, '02' AS feb, '03' AS mar)
  )
)
SELECT customer_id, jan, feb, mar,
       jan + feb + mar AS total,
       CASE WHEN jan + feb + mar > 
                 AVG(jan + feb + mar) OVER () 
            THEN 'HIGH' ELSE 'LOW' END AS segment
FROM pivoted;

パターン⑩ アンケート集計

-- 質問ごとに回答分布
SELECT question_id,
       satisfied,
       neutral,
       unsatisfied,
       satisfied + neutral + unsatisfied AS total,
       ROUND(satisfied * 100.0 / (satisfied + neutral + unsatisfied), 2) AS satisfaction_rate
FROM (
  SELECT question_id, response FROM survey_answers
)
PIVOT (
  COUNT(*)
  FOR response IN (
    'satisfied' AS satisfied,
    'neutral' AS neutral,
    'unsatisfied' AS unsatisfied
  )
);

パフォーマンス

PIVOT の内部動作

Oracle は PIVOT を内部的に CASE + GROUP BY に変換
→ パフォーマンスは基本的に同等

最適化のコツ

1. 早期の絞り込み:

-- ❌ 全データで PIVOT
SELECT * FROM (SELECT ... FROM huge_table) PIVOT (...);

-- ✅ WHERE で絞る
SELECT * FROM (
  SELECT ... FROM huge_table WHERE created_at > SYSDATE - 30
) PIVOT (...);

2. インデックス活用:

-- PIVOT の暗黙 GROUP BY 列にインデックス
CREATE INDEX ON sales(dept, product);

3. Materialized View 活用:

CREATE MATERIALIZED VIEW mv_monthly_sales
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND AS
SELECT * FROM (SELECT ... FROM orders) PIVOT (...);

CASE vs PIVOT の比較

-- ほぼ同じ性能
-- CASE のほうがコントロールしやすい
-- PIVOT のほうが簡潔

ベンチマークして選択。


Rails / Java / Python 対応

Rails ActiveRecord

# 生 SQL で PIVOT
sql = <<-SQL
  SELECT * FROM (SELECT dept, product, amount FROM sales)
  PIVOT (SUM(amount) FOR product IN ('A' AS a, 'B' AS b, 'C' AS c))
SQL

results = ActiveRecord::Base.connection.select_all(sql)

# または Rails 側で GROUP + Hash 変換
sales_by_dept_product = Sale.group(:dept, :product).sum(:amount)
# {[10, 'A'] => 100, [10, 'B'] => 200, ...}

# アプリ側で PIVOT
pivoted = sales_by_dept_product.group_by { |k, _| k[0] }
                                .transform_values { |v| v.to_h { |k, val| [k[1], val] } }

Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事、Solid Queue 使い方の記事も参照してください。

Java (JDBC)

String sql = """
    SELECT * FROM (SELECT dept, product, amount FROM sales)
    PIVOT (SUM(amount) FOR product IN ('A' AS a, 'B' AS b))
    """;

try (Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    ResultSetMetaData meta = rs.getMetaData();
    while (rs.next()) {
        // 動的列名で取得
        for (int i = 1; i <= meta.getColumnCount(); i++) {
            String colName = meta.getColumnName(i);
            System.out.println(colName + ": " + rs.getString(i));
        }
    }
}

Python (oracledb + pandas)

import oracledb
import pandas as pd

conn = oracledb.connect(...)

# Oracle 側で PIVOT
df = pd.read_sql("""
    SELECT * FROM (SELECT dept, product, amount FROM sales)
    PIVOT (SUM(amount) FOR product IN ('A' AS a, 'B' AS b))
""", conn)

# または Pandas で pivot
df_raw = pd.read_sql("SELECT dept, product, amount FROM sales", conn)
df_pivot = df_raw.pivot_table(
    index='dept', 
    columns='product', 
    values='amount', 
    aggfunc='sum',
    fill_value=0
)

大量データは Oracle 側で小規模は pandas 側で


実践シナリオ

シナリオ1:月次売上レポート

SELECT customer_id,
       NVL(jan, 0) AS jan,
       NVL(feb, 0) AS feb,
       NVL(mar, 0) AS mar,
       NVL(jan, 0) + NVL(feb, 0) + NVL(mar, 0) AS q1_total
FROM (
  SELECT customer_id, TO_CHAR(order_date, 'MM') month, amount
  FROM orders
  WHERE EXTRACT(YEAR FROM order_date) = 2026
)
PIVOT (
  SUM(amount) FOR month IN ('01' AS jan, '02' AS feb, '03' AS mar)
)
ORDER BY q1_total DESC;

シナリオ2:ダッシュボード KPI

SELECT metric_name,
       "OLD_VALUE",
       "NEW_VALUE",
       ROUND(("NEW_VALUE" - "OLD_VALUE") * 100.0 
             / NULLIF("OLD_VALUE", 0), 2) AS change_pct
FROM (
  SELECT metric_name, snapshot_date, metric_value
  FROM kpi_snapshots
  WHERE snapshot_date IN (SYSDATE - 7, SYSDATE)
)
PIVOT (
  MAX(metric_value)
  FOR snapshot_date IN (
    SYSDATE - 7 AS "OLD_VALUE",
    SYSDATE AS "NEW_VALUE"
  )
);

シナリオ3:ETL: 縦持ち→横持ち→縦持ち(クリーニング)

-- 元: 縦持ち(生データ)
-- 中間: 横持ち(PIVOT で分析)
-- 最終: 縦持ち(正規化して保存)

INSERT INTO monthly_metrics (customer_id, metric, value)
SELECT customer_id, metric, value
FROM (
  -- PIVOT でクレンジング
  SELECT * FROM raw_data
  PIVOT (
    MAX(amount) FOR metric_name IN ('rev' AS rev, 'ord' AS ord)
  )
)
-- UNPIVOT で正規化
UNPIVOT (
  value FOR metric IN (rev AS 'REVENUE', ord AS 'ORDERS')
);

MERGE 関連は Oracle MERGE 文 使い方の記事も参照してください。

シナリオ4:Data Pump 後のデータ整形

-- 別 DB から取り込んだ縦持ちデータを横持ちに
SELECT * FROM (
  SELECT customer_id, attribute_name, attribute_value
  FROM imported_data
)
PIVOT (
  MAX(attribute_value)
  FOR attribute_name IN (
    'name' AS customer_name,
    'email' AS email,
    'phone' AS phone
  )
);

Data Pump の詳細は Oracle Data Pump 使い方の記事を参照してください。

シナリオ5:Materialized View で高速レポート

CREATE MATERIALIZED VIEW mv_monthly_sales_pivoted
BUILD IMMEDIATE
REFRESH COMPLETE
NEXT SYSDATE + 1  -- 日次リフレッシュ
AS
SELECT customer_id, jan, feb, mar, apr, may, jun,
       jul, aug, sep, oct, nov, dec
FROM (
  SELECT customer_id, TO_CHAR(order_date, 'MM') month, amount
  FROM orders
)
PIVOT (
  SUM(amount)
  FOR month IN (
    '01' AS jan, '02' AS feb, '03' AS mar, '04' AS apr,
    '05' AS may, '06' AS jun, '07' AS jul, '08' AS aug,
    '09' AS sep, '10' AS oct, '11' AS nov, '12' AS dec
  )
);

-- BI ツールから高速アクセス
SELECT * FROM mv_monthly_sales_pivoted WHERE customer_id = 100;

シナリオ6:アンケート分析

-- 質問ごとの回答分布
SELECT question_id, question_text,
       COALESCE(strongly_agree, 0) AS "強く同意",
       COALESCE(agree, 0) AS "同意",
       COALESCE(neutral, 0) AS "中立",
       COALESCE(disagree, 0) AS "反対",
       COALESCE(strongly_disagree, 0) AS "強く反対"
FROM (
  SELECT q.question_id, q.question_text, r.response
  FROM questions q JOIN responses r ON q.question_id = r.question_id
)
PIVOT (
  COUNT(*)
  FOR response IN (
    'SA' AS strongly_agree,
    'A' AS agree,
    'N' AS neutral,
    'D' AS disagree,
    'SD' AS strongly_disagree
  )
);

シナリオ7:時系列データの UNPIVOT

-- 12 列(月次)を12行に正規化
INSERT INTO monthly_sales (customer_id, month, amount)
SELECT customer_id, TO_DATE(month, 'YYYY-MM'), amount
FROM legacy_wide_table
UNPIVOT (
  amount FOR month IN (
    "2026_01" AS '2026-01',
    "2026_02" AS '2026-02',
    "2026_03" AS '2026-03'
  )
);

シナリオ8:Rails ダッシュボードでの活用

class DashboardController < ApplicationController
  def monthly_sales
    sql = <<-SQL
      SELECT customer_id, NVL(jan, 0) jan, NVL(feb, 0) feb, NVL(mar, 0) mar
      FROM (SELECT customer_id, TO_CHAR(order_date, 'MM') month, amount FROM orders)
      PIVOT (SUM(amount) FOR month IN ('01' AS jan, '02' AS feb, '03' AS mar))
    SQL
    @data = ActiveRecord::Base.connection.select_all(sql)
    render json: @data
  end
end

シナリオ9:Docker Oracle での動作確認

docker exec -it oracle-xe sqlplus scott/tiger <<EOF
SELECT * FROM (
  SELECT deptno, job, sal FROM emp
)
PIVOT (
  SUM(sal) FOR job IN ('CLERK' AS clerk, 'MANAGER' AS mgr)
);
EOF

Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。

シナリオ10:Kamal デプロイ後の集計

-- デプロイ後の統計取得
SELECT * FROM (
  SELECT release_id, environment, status FROM deployments
  WHERE deploy_date > SYSDATE - 30
)
PIVOT (
  COUNT(*)
  FOR status IN (
    'SUCCESS' AS success,
    'FAILED' AS failed,
    'ROLLBACK' AS rollback
  )
);

Kamal 2 デプロイの詳細は Kamal 2 デプロイの記事を参照してください。


予防のベストプラクティス

1. サブクエリで必要な列だけ

-- ❌ 意図しない GROUP BY
SELECT * FROM (SELECT * FROM t) PIVOT (...);

-- ✅ 明示的
SELECT * FROM (SELECT col1, col2, col3 FROM t) PIVOT (...);

2. エイリアスを必ずつける

-- ✅ 別名で列名を制御
PIVOT (SUM(amount) FOR month IN ('01' AS jan, '02' AS feb))

3. NULL の扱い

-- COALESCE / NVL で 0 に
NVL(jan, 0) AS jan_sales

4. UNPIVOT の NULL

-- 意図に応じて選択
UNPIVOT INCLUDE NULLS  -- NULL 行も残す
UNPIVOT EXCLUDE NULLS  -- NULL 行を除外(デフォルト)

5. 動的 PIVOT の慎重な使用

- EXECUTE IMMEDIATE の複雑さ
- SQL Injection リスク
- パフォーマンス予測困難

BI ツール層で対応を優先検討。

6. パフォーマンステスト

EXPLAIN PLAN FOR SELECT ... PIVOT (...);
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

7. Materialized View 検討

-- 頻繁に参照するレポート
CREATE MATERIALIZED VIEW mv_report AS SELECT ... PIVOT (...);

8. WHERE で絞る

SELECT * FROM (
  SELECT ... FROM huge_table WHERE ...
) PIVOT (...);

9. インデックス設計

CREATE INDEX ON t(暗黙GROUP BY列);

10. コメントで意図明示

-- 月次売上をクロス集計してレポート表示用
SELECT * FROM (...) PIVOT (...);

トラブルシューティング

期待通りにならない

サブクエリの列を再確認:

-- 全列選択で意図しない GROUP BY 発生の可能性

列名がクォート付き

"'01'"  ← クォート付き列名になる

エイリアスで解決:

PIVOT (SUM(x) FOR y IN ('01' AS jan))

動的な列に対応したい

  • EXECUTE IMMEDIATE
  • PIVOT XML
  • BI ツール側で対応

CASE との性能差

-- ほぼ同じ性能
-- 可読性は PIVOT が上

UNPIVOT で NULL が消える

-- ✅ INCLUDE NULLS を明示
UNPIVOT INCLUDE NULLS (...)

複数値 PIVOT の順序

IN 句の順序が列順序になる:

IN ('A', 'B', 'C')  -- A, B, C の順

よくある質問(FAQ)

Q1. PIVOT はどのバージョンから?

Oracle 11g で導入。

Q2. 動的 PIVOT は可能?

通常構文では不可。EXECUTE IMMEDIATE or PIVOT XML。

Q3. CASE との使い分け

  • PIVOT: 簡潔、可読性
  • CASE: 柔軟、複雑な条件

Q4. PostgreSQL には?

crosstab 関数(tablefunc モジュール)。構文は異なる。

Q5. SQL Server との違い

構文は似ている、SQL Server にも PIVOT/UNPIVOT あり。

Q6. パフォーマンスは?

CASE と同等。内部的に同じ実行計画。

Q7. NULL の扱い

  • PIVOT: NULL 自動処理
  • UNPIVOT: INCLUDE/EXCLUDE 明示

Q8. 集約関数以外使える?

集約関数のみ(SUM, COUNT, AVG, MIN, MAX 等)。

Q9. WHERE との組み合わせ

SELECT * FROM (...) PIVOT (...) WHERE ...;
-- 外側で絞る

Q10. Rails での対応

生 SQL 経由。BI ツール活用も検討

Q11. Autonomous DB での対応

完全対応、自動最適化。

Q12. Excel との連携

エクセル形式に近い出力が PIVOT で簡単。CSV export で活用。


参考リンク

Oracle 公式


まとめ

Oracle PIVOT / UNPIVOT の要点を再整理します。

PIVOT vs UNPIVOT

項目PIVOTUNPIVOT
方向行 → 列列 → 行
集約必須不要
用途レポート正規化
NULL自動明示指定

基本構文

-- PIVOT
SELECT * FROM (<サブクエリ>)
PIVOT (
  <集約関数>(<列>)
  FOR <ピボット列> IN (<値> [AS <別名>], ...)
);

-- UNPIVOT
SELECT * FROM <テーブル>
UNPIVOT [INCLUDE NULLS | EXCLUDE NULLS] (
  <値列名>
  FOR <カテゴリ列名> IN (<列> [AS <ラベル>], ...)
);

5大パターン

-- ① 基本
PIVOT (SUM(x) FOR y IN ('A', 'B', 'C'))

-- ② エイリアス
PIVOT (SUM(x) FOR y IN ('A' AS a, 'B' AS b))

-- ③ 複数集約
PIVOT (SUM(x) AS total, COUNT(*) AS cnt FOR y IN ('A' AS a))

-- ④ 複数ピボット列
PIVOT (SUM(x) FOR (y, z) IN ((1, 'A'), (2, 'B')))

-- ⑤ UNPIVOT
UNPIVOT (val FOR cat IN (col1, col2, col3))

10大実践パターン

#パターン
月次売上クロス
部署×職種ヘッドカウント
NULL を 0 変換
複数集計値
PIVOT-UNPIVOT 連携
年別トレンド
Cohort 分析
Yes/No 表示
分析関数併用
アンケート集計

動的 PIVOT

- EXECUTE IMMEDIATE(PL/SQL)
- PIVOT XML(結果 XML 型)
- LISTAGG で代替
- BI ツール層で対応

予防のベストプラクティス

  • サブクエリで必要な列だけ
  • エイリアスを必ずつける
  • NULL 処理を明示
  • UNPIVOT で INCLUDE/EXCLUDE 明示
  • 動的 PIVOT は慎重に
  • パフォーマンステスト
  • Materialized View 検討
  • WHERE で早期絞り込み

事故防止

  • 暗黙 GROUP BY の罠(サブクエリで全列選択しない)
  • IN 句の値の順序 = 列の順序
  • クォート付き列名を避ける
  • NULL のデフォルト挙動確認

モダン Oracle の SQL 三種の神器

✅ 分析関数(OVER/PARTITION BY)
✅ MERGE 文(UPSERT)
✅ PIVOT / UNPIVOT ← 本記事

これらの知識は、Oracle での KPI レポート・ダッシュボード作成・ETL・BI 連携・データ正規化・アンケート分析・Rails / Java / Python 開発など、あらゆる場面で活用できます。本記事をブックマークしておけば、Oracle PIVOT/UNPIVOT を確実に使いこなせるようになります。


本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。