本文へ移動
SQL道場 更新日: 2026年5月2日 約19分で読めます

PIVOT・UNPIVOTをSQLで実装する方法|CASE WHENで縦横変換する

データは「縦長」で持ち、「横長」で見る

SQLのテーブルは基本的に縦長(ロング形式)で設計される。月次売上なら「1行 = 1カテゴリ × 1ヶ月」という形だ。これは集計しやすく、行を追加するだけで新しいデータを格納できる。

しかし人間がレポートを読むときは「横長(ワイド形式)」が直感的に分かりやすい。月を列に並べることで、カテゴリごとの月次推移が一目で見える。

カテゴリ売上
ヘアケア10月3,200,000
ヘアケア11月3,800,000
ヘアケア12月4,100,000
スキンケア10月2,400,000
スキンケア11月2,900,000
スキンケア12月3,200,000
縦長
カテゴリ10月11月12月
ヘアケア3,200,0003,800,0004,100,000
スキンケア2,400,0002,900,0003,200,000
横長

この変換を「ピボット(縦→横)」、逆方向を「アンピボット(横→縦)」と呼ぶ。

SQL Server や BigQuery には PIVOT / UNPIVOT 構文が存在するが、や多くの標準 SQL エンジンにはない。代わりに CASE WHEN を使って実装する方法を今回は完全に解説する。


使用するテーブル

-- monthly_category_sales(月次カテゴリ別売上:縦長形式)
-- category    : カテゴリ名
-- sales_month : 集計月(DATE型、月初の日付)
-- total_sales : 売上金額
-- order_count : 注文件数
categorysales_monthtotal_salesorder_count
ヘアケア2024-10-013,200,000284
ヘアケア2024-11-013,800,000341
ヘアケア2024-12-014,100,000378
スキンケア2024-10-012,400,000198
スキンケア2024-11-012,900,000241
スキンケア2024-12-013,200,000276
ボディケア2024-10-011,800,000156
ボディケア2024-11-012,100,000182
ボディケア2024-12-012,400,000208

Part 1:ピボット(縦→横変換)

STEP 1 ― CASE WHEN で月を列に展開する

各月を1列にするには「その月のデータであれば値を返し、そうでなければ NULL を返す」CASE WHEN を月ごとに書き、MAX(または SUM)で集約する。

-- STEP1: 3ヶ月分を横展開する基本ピボット

SELECT
    category,

    -- 10月の売上
    MAX(CASE WHEN sales_month = DATE '2024-10-01' THEN total_sales END)  AS "2024_10",

    -- 11月の売上
    MAX(CASE WHEN sales_month = DATE '2024-11-01' THEN total_sales END)  AS "2024_11",

    -- 12月の売上
    MAX(CASE WHEN sales_month = DATE '2024-12-01' THEN total_sales END)  AS "2024_12"

FROM monthly_category_sales
GROUP BY category
ORDER BY category;

出力イメージ

category2024_102024_112024_12
ヘアケア3,200,0003,800,0004,100,000
スキンケア2,400,0002,900,0003,200,000
ボディケア1,800,0002,100,0002,400,000

MAXSUM の使い分け
1つの category × month の組み合わせに必ず1行しかない場合は MAX でも SUM でも結果は同じだ。しかし集計前のデータ(行が重複している可能性がある)に直接ピボットをかける場合は SUM を使う。今回のような「すでに集計済みの縦長テーブル」には MAX が直感的でシンプルだ。

STEP 2 ― 元データから直接ピボットする(orders テーブルから)

集計済みテーブルがなく、ordersorder_items から直接ピボットするパターンも実務では多い。この場合は SUM を使う。

-- STEP2: 元データから直接ピボット(SUM を使う)

SELECT
    oi.category,

    SUM(CASE
        WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-10-01'
        THEN oi.quantity * oi.unit_price
        ELSE 0
    END)  AS "2024_10",

    SUM(CASE
        WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-11-01'
        THEN oi.quantity * oi.unit_price
        ELSE 0
    END)  AS "2024_11",

    SUM(CASE
        WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-12-01'
        THEN oi.quantity * oi.unit_price
        ELSE 0
    END)  AS "2024_12",

    -- 合計列も一緒に出す
    SUM(oi.quantity * oi.unit_price)  AS "合計"

FROM orders       o
JOIN order_items  oi  ON o.order_id = oi.order_id
WHERE
    o.status = 'completed'
    AND o.order_date >= DATE '2024-10-01'
    AND o.order_date <  DATE '2025-01-01'
GROUP BY oi.category
ORDER BY 合計 DESC;

出力イメージ

category2024_102024_112024_12合計
ヘアケア3,200,0003,800,0004,100,00011,100,000
スキンケア2,400,0002,900,0003,200,0008,500,000
ボディケア1,800,0002,100,0002,400,0006,300,000

STEP 3 ― 前月比も同時に出す(完成版ピボット)

ピボットしながら前月比も一緒に計算する。月の列と成長率の列を交互に並べることで、レポートとして完結する。

-- STEP3: 売上と前月比を交互に並べた完成版ピボット

WITH monthly_pivot AS (
    SELECT
        oi.category,
        SUM(CASE WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-10-01'
                 THEN oi.quantity * oi.unit_price ELSE 0 END)  AS m10,
        SUM(CASE WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-11-01'
                 THEN oi.quantity * oi.unit_price ELSE 0 END)  AS m11,
        SUM(CASE WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-12-01'
                 THEN oi.quantity * oi.unit_price ELSE 0 END)  AS m12
    FROM orders       o
    JOIN order_items  oi  ON o.order_id = oi.order_id
    WHERE o.status = 'completed'
    AND o.order_date >= DATE '2024-10-01'
    AND o.order_date <  DATE '2025-01-01'
    GROUP BY oi.category
)
SELECT
    category,
    m10                                                         AS "10月売上",
    m11                                                         AS "11月売上",
    ROUND((m11 - m10) * 100.0 / NULLIF(m10, 0), 1)            AS "11月前月比%",
    m12                                                         AS "12月売上",
    ROUND((m12 - m11) * 100.0 / NULLIF(m11, 0), 1)            AS "12月前月比%",
    m10 + m11 + m12                                             AS "3ヶ月合計"
FROM monthly_pivot
ORDER BY "3ヶ月合計" DESC;

完成した出力イメージ

category10月売上11月売上11月前月比%12月売上12月前月比%3ヶ月合計
ヘアケア3,200,0003,800,000+18.84,100,000+7.911,100,000
スキンケア2,400,0002,900,000+20.83,200,000+10.38,500,000
ボディケア1,800,0002,100,000+16.72,400,000+14.36,300,000

Part 2:アンピボット(横→縦変換)

横長のデータを縦長に戻す操作だ。BIツールが縦長形式を要求するとき、Excelから受け取ったレポートをDBに取り込むとき、横長テーブルに GROUP BY をかけたいときなどに使う。

STEP 4 ― CROSS JOIN + CASE WHEN でアンピボット

横長テーブルから縦長に変換する標準的なアプローチは CROSS JOIN + 「どの列を取り出すか」の制御だ。

-- STEP4: 横長テーブルを縦長に変換する(アンピボット)

-- 元の横長テーブル(monthly_pivot_wide)
-- category | 2024_10 | 2024_11 | 2024_12
-- ヘアケア  | 3,200,000 | 3,800,000 | 4,100,000

WITH wide_table AS (
    -- 上記のSTEP3のmonthly_pivotをそのまま利用
    SELECT
        oi.category,
        SUM(CASE WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-10-01'
                 THEN oi.quantity * oi.unit_price ELSE 0 END)  AS m10,
        SUM(CASE WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-11-01'
                 THEN oi.quantity * oi.unit_price ELSE 0 END)  AS m11,
        SUM(CASE WHEN DATE_TRUNC('month', o.order_date) = DATE '2024-12-01'
                 THEN oi.quantity * oi.unit_price ELSE 0 END)  AS m12
    FROM orders o JOIN order_items oi ON o.order_id = oi.order_id
    WHERE o.status = 'completed'
    AND o.order_date >= DATE '2024-10-01'
    AND o.order_date <  DATE '2025-01-01'
    GROUP BY oi.category
),
-- 月のリストをCROSS JOINで展開
month_list AS (
    SELECT 1 AS month_id, DATE '2024-10-01' AS sales_month, '2024_10' AS month_label
    UNION ALL
    SELECT 2, DATE '2024-11-01', '2024_11'
    UNION ALL
    SELECT 3, DATE '2024-12-01', '2024_12'
)
SELECT
    w.category,
    m.sales_month,
    m.month_label,
    -- month_idに応じて対応する列の値を取り出す
    CASE m.month_id
        WHEN 1 THEN w.m10
        WHEN 2 THEN w.m11
        WHEN 3 THEN w.m12
    END  AS total_sales
FROM wide_table   w
CROSS JOIN month_list  m
ORDER BY w.category, m.month_id;

出力イメージ(縦長形式に戻った)

categorysales_monthtotal_sales
ヘアケア2024-10-013,200,000
ヘアケア2024-11-013,800,000
ヘアケア2024-12-014,100,000
スキンケア2024-10-012,400,000
スキンケア2024-11-012,900,000
スキンケア2024-12-013,200,000

元の縦長形式に戻った。CROSS JOIN × CASE のパターンがアンピボットの定石だ。


動的列数への対応

CASE WHEN によるピボットの最大の弱点は「列数があらかじめ決まっていないと書けない」ことだ。月数が固定なら問題ないが、「直近N ヶ月を動的に展開したい」という要件には対応が難しい。

現実的な対処法は2つある。

対処法1:列数の上限を決めて CASE WHEN を多めに書いておく

-- 直近12ヶ月分のCASE WHENを書いておき、
-- 対象期間外はNULLになる(BIツール側でNULL列を非表示にする)

SELECT
    category,
    MAX(CASE WHEN sales_month = DATE_ADD('month', -11, DATE_TRUNC('month', CURRENT_DATE))
             THEN total_sales END)  AS m_minus_11,
    MAX(CASE WHEN sales_month = DATE_ADD('month', -10, DATE_TRUNC('month', CURRENT_DATE))
             THEN total_sales END)  AS m_minus_10,
    -- ... m_minus_9 〜 m_minus_1 ...
    MAX(CASE WHEN sales_month = DATE_TRUNC('month', CURRENT_DATE)
             THEN total_sales END)  AS m_current
FROM monthly_category_sales
GROUP BY category;

対処法2:縦長形式のままBIツールに渡してピボットはBIツール側で行う

Tableau・Looker Studio・Metabase などのBIツールはピボット機能を持っている。SQLはデータを縦長で出力することに徹し、横展開はBIツールに任せるほうが柔軟性が高く、運用コストも低い。

実務では「固定期間のレポートにはSQLでピボット」「動的期間の分析にはBIツールでピボット」と使い分けるのが現実的だ。


ピボットとコホートテーブルの関係

実は No.4(コホート分析)の STEP4 で書いた「ピボット形式のコホートテーブル」は、今回の CASE WHEN ピボットそのものだった。

-- No.4のコホートピボット(再掲・今回の手法との共通点)
SELECT
    cohort_month,
    MAX(CASE WHEN month_number = 0 THEN retention_rate END)  AS "M+0",
    MAX(CASE WHEN month_number = 1 THEN retention_rate END)  AS "M+1",
    MAX(CASE WHEN month_number = 2 THEN retention_rate END)  AS "M+2",
    -- ...
FROM retention
GROUP BY cohort_month;

month_number という数値を列名に変換して横展開する」のが CASE WHEN ピボットの本質だ。コホートテーブル・ヒートマップ・クロス集計表など、あらゆる「行列形式の出力」が同じパターンで実現できる。


実務での運用ヒント

① NULL と 0 の使い分け

MAX(CASE WHEN ... THEN total_sales END) で対象データがない場合は NULL が返る。「売上ゼロ」と「データなし(その月にそのカテゴリの注文がなかった)」を区別したい場合は NULL のままにする。区別不要なら COALESCE(..., 0) で 0 に変換する。

② 列名に日付文字列を使う場合はクォートが必要

"2024_10" のように数字で始まる列名や特殊文字を含む列名はダブルクォートで囲む必要がある。Presto では "2024_10" は有効だが、2024-10 のようなハイフンを含む列名はエラーになる。アンダースコアに変換するか、m10 のような短いエイリアスを使うのが安全だ。

③ 12ヶ月以上のピボットはSQL以外の選択肢を検討する

12ヶ月 × 複数カテゴリを CASE WHEN で全部書くと、クエリが数百行になる。メンテナンス性が著しく落ちるため、Pythonの pandas.pivot_table やBIツールのピボット機能に委ねたほうが現実的だ。SQLの CASE WHEN ピボットは「3〜6列程度の固定レポート」に向いている。


まとめ

変換の方向手法核心パターン
縦→横(ピボット)SUM(CASE WHEN col = 'X' THEN val END) + GROUP BY月や区分を「列名」に変換
横→縦(アンピボット)CROSS JOIN month_list + CASE month_id WHEN N THEN col列を「行」に展開

ピボットは「どの列をGROUP BYの軸にし、どの値を列方向に展開するか」を明確に決めてから書き始めると迷わない。アンピボットは「展開したい列の一覧を month_list のようなCTEで用意し、CROSS JOIN する」という構造を覚えると応用が利く。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む