【完全ガイド】Oracle PIVOT / UNPIVOT 徹底解説|クロス集計・動的PIVOT・実践パターンまで
- 作成日 2026.07.24
- Oracle Database
Oracle SQL でレポート作成に必須のスキル、PIVOT と UNPIVOT:
-- 部署 × 職種のクロス集計
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
| 項目 | PIVOT | UNPIVOT |
|---|---|---|
| 方向 | 行 → 列(横持ち) | 列 → 行(縦持ち) |
| 集約 | 必須(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
| 項目 | PIVOT | UNPIVOT |
|---|---|---|
| 方向 | 行 → 列 | 列 → 行 |
| 集約 | 必須 | 不要 |
| 用途 | レポート | 正規化 |
| 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)もあわせてご確認ください。
-
前の記事
【完全ガイド】ORA-00600: internal error code の原因と解決方法|引数の読み方・MOS 検索・AHF/TFA まで徹底解説 2026.07.24
-
次の記事
【完全ガイド】Oracle NUMBER型 vs INTEGER の違い|精度・スケール・BINARY_DOUBLE・PLS_INTEGER 徹底解説 2026.07.24
コメントを書く