動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

SQL Server の PIVOT / UNPIVOT で業務帳票のクロス集計表を作る

バイブス父さん
現役の業務SE
2026年7月12日9 min read
SQL Server の PIVOT / UNPIVOT で業務帳票のクロス集計表を作る

みなさんこんにちは!ヒロポンです!

「月ごとの売上を、商品を縦・月を横にした表で出してほしい」。業務系だと、この手の帳票依頼、しょっちゅう来ますよね??

データは縦持ち(1行1レコード)で持ってる。なのに帳票は横持ちで、月が列にずらっと並ぶ。この縦→横の変換、GROUP BYCASE を延々並べて手書きしてる人、けっこう多いはずです。

SQL Server には、これを一発でやる PIVOT があります。今回は SQL Server の PIVOT の使い方を、売上テーブルのクロス集計を例にたどっていきます。横持ちを縦持ちに戻す UNPIVOT、それに列が増える時の動的 SQL まで、コピペで動く形で潰しましょう。

結論: 縦→横は PIVOT、横→縦は UNPIVOT

先に結論から。

  • 縦持ち → 横持ち(帳票のクロス集計)は PIVOT
  • 横持ち → 縦持ち(正規化された形に戻す)は UNPIVOT

PIVOT は、指定した列の値を「列そのもの」に展開して、値を集計してくれる T-SQL の演算子です。イメージはこんな感じ。

縦持ちの売上データを PIVOT で横持ちのクロス集計表にし、UNPIVOT で縦持ちに戻す双方向の変換

実際のテーブルで見ていきます。

サンプルの売上テーブル(縦持ち)

商品×月の売上を縦持ちで持ってる、こういうテーブルを使います。

CREATE TABLE Sales (
    Product   NVARCHAR(20),
    SaleMonth INT,
    Amount    INT
);

INSERT INTO Sales (Product, SaleMonth, Amount) VALUES
(N'りんご', 1, 100), (N'りんご', 2, 120), (N'りんご', 3, 150),
(N'みかん', 1,  80), (N'みかん', 2,  90), (N'みかん', 3,  70),
(N'ぶどう', 1, 200), (N'ぶどう', 2, 210), (N'ぶどう', 3, 190);

9行、縦に並んでます。これを「商品を行、月を列」の帳票にしていきます。

ひとつ寄り道すると、日本語を NVARCHAR の列に入れる時は、文字列の頭に N を付けて N'りんご' と書きます。忘れると、DB の照合順序によっては日本語が ? に化けて格納される。地味ですけど、けっこう大事なやつです。

PIVOT で縦持ちを横持ちのクロス集計表にする

PIVOT を使うとこんな感じで書けます。

SELECT Product AS 商品, [1] AS [1月], [2] AS [2月], [3] AS [3月]
FROM (
    SELECT Product, SaleMonth, Amount FROM Sales
) AS src
PIVOT (
    SUM(Amount) FOR SaleMonth IN ([1], [2], [3])
) AS pvt
ORDER BY 商品;

結果はこうなります。

商品     1月   2月   3月
------   ----  ----  ----
ぶどう   200   210   190
みかん    80    90    70
りんご   100   120   150

実際に SQL Server で実行した結果がこれです。

SQL Server で PIVOT を実行したクロス集計表の実行結果。商品を行、1月〜3月を列に売上が並んでいる

縦に9行あったデータが、商品ごと3行の横持ちに変わりました。いい感じにクロス集計できてます。

ポイントは3つ。

  • SUM(Amount) … 集計する値と集計関数
  • FOR SaleMonth … どの列の値を列に展開するか
  • IN ([1], [2], [3]) … 展開する値(=新しい列名)を列挙する

この IN (...) に「1月, 2月, 3月」を手で書いてる。ここが、あとで効いてきます。

PIVOT は列を静的に列挙しないといけない(動的 PIVOT)

さっきの IN ([1], [2], [3])。ここ、実行時に列を決められません。SQL を書く時点で「どの月を列にするか」を固定で書く必要があります。

ん? 月が増えたらどうすんの??って引っかかりますよね。4月、5月とデータが増えても、IN (...) に手で足さない限り列は出てきません。

業務でいちばん最初に詰まるのが、まさにここ。対処は、列リストを文字列で組み立てて、動的 SQL で実行するやり方です。

DECLARE @cols NVARCHAR(MAX), @sql NVARCHAR(MAX);

-- 実在する月を [1],[2],[3] の形で組み立てる(SQL Server 2016 でも動く)
SELECT @cols = STUFF((
    SELECT DISTINCT ',' + QUOTENAME(SaleMonth)
    FROM Sales
    ORDER BY ',' + QUOTENAME(SaleMonth)
    FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '');

SET @sql = N'
SELECT Product AS 商品, ' + @cols + N'
FROM (SELECT Product, SaleMonth, Amount FROM Sales) AS src
PIVOT (SUM(Amount) FOR SaleMonth IN (' + @cols + N')) AS pvt
ORDER BY 商品;';

EXEC sp_executesql @sql;

これなら、月が増えても勝手に列が増えます。STUFF + FOR XML PATH は列名を1つの文字列に連結する定番のイディオムで、SQL Server 2016 でも動きます。

SQL Server 2017 以降なら、@cols の組み立ては STRING_AGG(QUOTENAME(SaleMonth), ',') でもっと短く書けます。現場のバージョンに合わせてください。

GROUP BY + CASE で手書きする方法

PIVOT を使わず、GROUP BYCASE で同じ表を作ることもできます。というか、昔からあるのはこっちの書き方。

SELECT Product AS 商品,
       SUM(CASE WHEN SaleMonth = 1 THEN Amount ELSE 0 END) AS [1月],
       SUM(CASE WHEN SaleMonth = 2 THEN Amount ELSE 0 END) AS [2月],
       SUM(CASE WHEN SaleMonth = 3 THEN Amount ELSE 0 END) AS [3月]
FROM Sales
GROUP BY Product
ORDER BY Product;

結果は PIVOT 版とまったく同じ。冗長に見えますけど、こっちにも利点があります。

PIVOT と GROUP BY + CASE を書きやすさ・列の固定/動的・移植性・複数集計で比較した表

CASE 版は「1月の売上」と「1月の件数」みたいに、複数の集計を1クエリで並べたい時に強い。PIVOT は1回に1集計なので、そこは CASE の方が素直に書けます。まあ使い分けですね。

UNPIVOT で横持ちを縦持ちに戻す

逆に、横持ちの帳票テーブルを縦持ちに正規化したい時は UNPIVOT の出番です。

-- 横持ちの帳票テーブル(例)
CREATE TABLE CrossTab (
    商品 NVARCHAR(20), [1月] INT, [2月] INT, [3月] INT
);
INSERT INTO CrossTab (商品, [1月], [2月], [3月]) VALUES
(N'りんご', 100, 120, 150),
(N'みかん',  80,  90,  70);

SELECT 商品, 月, 売上
FROM CrossTab
UNPIVOT (
    売上 FOR 月 IN ([1月], [2月], [3月])
) AS unpvt
ORDER BY 商品, 月;

実行結果はこうなります。

UNPIVOT で横持ちのクロス集計表を縦持ちに戻した実行結果。商品・月・売上の3列に展開されている

[1月] [2月] [3月] の3列が、売上 の2列に縦展開されました。CSV で受け取った横持ちデータを DB に正規化して取り込む、そういう場面で効きます。

ハマりポイント(知らないと地味にハマる)

私が実際に踏んだやつを3つ。

① サブクエリで列を絞らないと、粒度が崩れる

PIVOT は、FROM に出てくる「集計値・展開キー以外の列」を全部グループ化キーにします

だから FROM Sales を直に書くと、余計な列があった時にそれが行キーになって、意図しない粒度で分かれてしまう。上の例で SELECT Product, SaleMonth, Amount FROM Sales とサブクエリで必要な3列だけに絞ってるのは、これを防ぐためです。地味だけど効きます。

② データが無いセルは NULL になる

ある商品にその月のデータが無いと、そのセルは 0 じゃなく NULL で返ってきます。帳票で「空欄」を「0」で出したいなら、ISNULL([1月], 0) みたいに外側で埋めましょう。

③ UNPIVOT は列の型を揃える

UNPIVOT で縦にする列は、データ型が揃ってないとダメINTNVARCHAR を混ぜると暗黙変換が走るか、エラーで止まります。金額と商品名を一緒に縦展開しようとして詰まる、これが定番です。

まとめ

SQL Server のクロス集計、こう覚えておけば現場で困りません。

  • 縦 → 横(帳票)は PIVOT、横 → 縦は UNPIVOT
  • PIVOT の列は静的列挙が基本。月が増えるなら動的 SQL
  • 複数集計や移植性が要るなら GROUP BY + CASE

帳票のクロス集計が SQL 一発で出せると、Excel に貼って手で組み替えてた作業がまるっと消えます。まずはサンプルテーブルをコピペして、PIVOT を動かしてみてください!!

よくある質問

Q1. PIVOT で列を動的に増やすにはどうすればいいですか?

PIVOTIN 句は列名を静的に書く必要があるので、列を文字列で組み立てて sp_executesql で実行する動的 SQL を使います。列リストは SQL Server 2016 なら STUFF + FOR XML PATH、2017 以降なら STRING_AGG で作れます。

Q2. PIVOT と GROUP BY + CASE はどちらを使うべきですか?

列が固定の帳票なら PIVOT が短く書けます。列が動的・複数集計を同時に出す・他 DB への移植性が要るなら GROUP BY + CASE の方が柔軟です。

Q3. PIVOT のセルが NULL になるのはなぜですか?

その組み合わせのデータが元テーブルに無いからです。0 で埋めたいなら PIVOT の外側で ISNULLCOALESCE を使ってください。

Q4. UNPIVOT で横持ちを縦持ちに戻せますか?

戻せます。縦にしたい列を IN 句に列挙します。ただし対象の列はデータ型を揃える必要があります。

次に読むべき記事

以上!

同じ帳票でハマってる人がいたら、どんどんシェア待ってるぜ!!

この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について