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

SQL Server の ROLLUP / GROUPING SETS で帳票の小計・総計行を出す

バイブス父さん
現役の業務SE
2026年8月6日15 min read広告 (PR) を含む場合があります
SQL Server の ROLLUP / GROUPING SETS で帳票の小計・総計行を出す

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

帳票の SQL でこういう注文、来ませんか??

「部署ごとの合計と、全体の総計を、同じ一覧に出しといて」。

明細があって、部署ごとの小計があって、いちばん下に総計。Excel なら小計機能でポチポチやるやつです。SQL でやろうとすると、最初はだいたい GROUP BY の結果に UNION ALL で総計行を自前で足す、みたいな書き方になる。俺も昔そうしてました。

でも SQL Server には、GROUP BY に小計・総計を足すための専用構文がある。それが ROLLUP と GROUPING SETS です。

今回はこの2つがそもそも何を解決するのか、そして「小計行の集計キーが NULL になる」という最初に引っかかる点を GROUPING() でどう捌くのか。Docker の SQL Server で実際に動かしながら積み上げていきます。

忙しい人向けに最初にまとめ

  • GROUP BY ROLLUP(部署, 商品) と書くと、明細に加えて部署ごとの小計全体の総計が自動で足される
  • 小計・総計行は、集計された列が NULL になる。だからデータ本来の NULL と見分けがつかない
  • 見分けるのが GROUPING(列)。小計・総計行なら 1、明細行なら 0 を返す。CASE WHEN GROUPING(部署)=1 THEN '総計' ELSE 部署 END でラベルを付ける
  • 階層に沿わない特定の小計だけ欲しいなら GROUPING SETS で組み合わせを列挙する
  • UNION ALL で総計を自前で足すより、ROLLUP のほうが短くて速い

コードは全部この記事の中にあります。まず「何を解決する道具なのか」から。

この構文が解決する課題 — 小計・総計を SQL 一発で

ROLLUP を知らない時代の書き方を、まず思い出しておきます。ここが出発点です。

部署ごとの合計と全体の総計を、1つの結果に出したい。ROLLUP を使わないと、こうなりがちです。

-- ROLLUP を知らない頃の書き方: 総計を UNION ALL で自前で足す
SELECT Department, SUM(Amount) AS Total
FROM Sales
GROUP BY Department
UNION ALL
SELECT N'総計', SUM(Amount)
FROM Sales;

動くには動く。でも同じテーブルを2回スキャンしてるし、部署と商品の2段階の小計まで欲しくなったら UNION ALL がどんどん増える。3段階になったら、もう目も当てられない。

ROLLUP は、この「小計・総計を足す」という作業を構文レベルで持っている道具です。GROUP BY の後ろに ROLLUP(...) と書くだけで、集計エンジンが小計行を勝手に組み立ててくれる。テーブルスキャンも1回で済む。

ここまでで分かったこと: 小計・総計を UNION ALL で自前で足すのは、スキャンも記述も増える。SQL Server にはそれ専用の ROLLUP / GROUPING SETS がある。

まず動かす土台 — サンプルテーブルを用意する

以降のコードは、この一時テーブルを前提に動かします。最初にこれだけ流しておいてください。

-- 部署 × 商品カテゴリ × 金額 の売上サンプル
DROP TABLE IF EXISTS #Sales;
CREATE TABLE #Sales (
    Department NVARCHAR(20),
    Category   NVARCHAR(20),
    Amount     INT
);
INSERT INTO #Sales (Department, Category, Amount) VALUES
(N'営業部', N'ハード', 120),
(N'営業部', N'ソフト',  80),
(N'開発部', N'ハード',  60),
(N'開発部', N'ソフト', 200);

営業部が合計 200、開発部が合計 260、全体で 460。この数字が小計・総計行として出てくるかを見ていきます。

ROLLUP の基本 — GROUP BY に小計と総計を足す

いちばんシンプルな形から。部署だけで ROLLUP してみます。

SELECT Department, SUM(Amount) AS Total
FROM #Sales
GROUP BY ROLLUP(Department);

これで営業部 200・開発部 260 に加えて、Department が NULL の行に 460(総計)が付いてきます。ROLLUP(Department) は「部署ごと」と「部署をまとめた総計」の2階層を出す、という意味です。

もう一段深くして、部署 × 商品でやってみます。ここが帳票っぽくなるところ。「あれ、この NULL の行なんや??」ってなるのも、ちょうどここです。

SELECT Department, Category, SUM(Amount) AS Total
FROM #Sales
GROUP BY ROLLUP(Department, Category)
ORDER BY GROUPING(Department), Department, GROUPING(Category), Category;

実行結果:

ROLLUP(Department, Category) の結果。明細行に加え、Category が NULL の部署小計行と、Department も Category も NULL の総計行が出ている

明細(営業部/ハード=120 など)の下に、Category が NULL の行 = 部署ごとの小計(営業部の小計 200、開発部の小計 260)、いちばん下に DepartmentCategory も NULL の行 = 総計 460 が出ています。

ROLLUP(Department, Category) は、右から順に列を畳んでいく。「部署×商品」→「部署ごと(商品をまとめた小計)」→「全部まとめた総計」という階層です。ORDER BYGROUPING() を入れているのは、小計・総計を各グループの下に並べて帳票の見た目にするため。

ここまでで分かったこと: ROLLUP(A, B) は右から列を畳んで、Bの小計・Aの小計・総計を階層的に足す。小計・総計行では畳まれた列が NULL になる。

GROUPING() で「小計の NULL」と「本物の NULL」を見分ける

ここが業務SEが最初に引っかかりやすいところ。

小計・総計行では、集計された列が NULL で出てくる。でも、ちょっと考えてみてください。もし Department にデータ本来の NULL(部署未設定の売上とか)があったら?? 小計行の NULL とデータの NULL が、見た目でまったく区別つかないんです。

WHERE Department IS NULL で総計行だけ取ろうとすると、部署未設定の明細まで混ざってくる。ここで詰まる。

これを解決するために用意されているのが GROUPING() です。

GROUPING(列) は、その行がその列で集計された小計・総計行なら 1、通常の明細行なら 0 を返します。NULL かどうかではなく、「ロールアップで畳まれた行かどうか」を直接教えてくれる。

これを使って、NULL のかわりにちゃんとしたラベルを付けます。

SELECT
    CASE WHEN GROUPING(Department) = 1 THEN N'★総計'
         ELSE Department END AS Department,
    CASE WHEN GROUPING(Category) = 1 AND GROUPING(Department) = 0 THEN N'(部署小計)'
         WHEN GROUPING(Category) = 1 THEN N''
         ELSE Category END AS Category,
    SUM(Amount) AS Total
FROM #Sales
GROUP BY ROLLUP(Department, Category)
ORDER BY GROUPING(Department), Department, GROUPING(Category), Category;

実行結果:

GROUPING() でラベル付けした結果。部署小計行に「(部署小計)」、総計行に「★総計」が表示され、NULL が消えている

NULL が消えて、部署の小計行に「(部署小計)」、いちばん下に「★総計」が付きました。こんな感じで、これならもう帳票にそのまま流せます!!

肝は、GROUPING(Category)=1 AND GROUPING(Department)=0 で「商品は畳まれたが部署は畳まれていない行」=部署小計を狙い撃ちしていること。両方 1 なら総計、という判定です。列が2つ以上ある時は GROUPING_ID() でまとめて判定する手もありますが、まずは GROUPING() の組み合わせで十分わかります。

ここまでで分かったこと: 小計・総計行の NULL は、データの NULL と見分けがつかない。GROUPING(列) が「畳まれた行か」を 1 / 0 で返すので、これでラベルを付けたり総計行だけ抽出したりする。

GROUPING SETS — 欲しい小計だけを狙って出す

ROLLUP は「階層的に全部の小計」を出します。便利なんだけど、階層に沿わない小計が欲しい時には向かない。

たとえば「部署ごとの小計」と「商品ごとの小計」は欲しいけど、「部署×商品の明細」は要らない、というケース。ROLLUP だと明細まで付いてくる。こういう時が GROUPING SETS の出番です。

GROUPING SETS は、欲しい集計の組み合わせを自分で列挙する書き方です。

SELECT
    CASE WHEN GROUPING(Department) = 1 THEN N'(全社)' ELSE Department END AS Department,
    CASE WHEN GROUPING(Category)   = 1 THEN N'(全商品)' ELSE Category   END AS Category,
    SUM(Amount) AS Total
FROM #Sales
GROUP BY GROUPING SETS ( (Department), (Category), () )
ORDER BY GROUPING(Department), GROUPING(Category);

実行結果:

GROUPING SETS の結果。部署ごとの小計、商品ごとの小計、全体の総計だけが出て、部署×商品の明細は含まれていない

(Department) で部署ごと、(Category) で商品ごと、()(空の集合)で総計。明細(部署×商品)は列挙してないので出てきません。こんな感じで、狙った小計だけがきれいに並びます。

ちなみに ROLLUP(Department, Category) は、GROUPING SETS ( (Department, Category), (Department), () ) と書くのと同じです。だから ROLLUP は GROUPING SETS の「階層版ショートカット」、と理解しておくと、2つの関係がスッと入ります。

ここまでで分かったこと: GROUPING SETS は欲しい集計の組み合わせを自分で列挙する。() は総計。ROLLUP はその階層パターンのショートカット。

ROLLUP と UNION ALL、書くとどう違うか

最初に出した UNION ALL の書き方と、ROLLUP を並べて見てみます。

UNION ALL で総計を自前で足す書き方から、GROUP BY ROLLUP に置き換える before-after

やってることは同じ「部署ごと+総計」。でも ROLLUP 版はテーブルスキャンが1回で、行数も短い。小計の階層が増えても ROLLUP(A, B, C) と列を足すだけで済みます。UNION ALL 版はそのたびにクエリブロックが増えていく。

使い分けを一枚に整理すると、こう。

GROUP BY のみ・ROLLUP・GROUPING SETS を小計行・総計行・柔軟性・記述量・向いてる場面で比較した表

ここまでで分かったこと: 階層的な小計は ROLLUP、狙った組み合わせだけは GROUPING SETS、明細だけなら素の GROUP BY。UNION ALL の自前積み上げは基本卒業していい。

俺の現場ではこう使ってる

概念が入ったところで、実務でどう効くかを1つ。

流通系の基幹保守で、月次の売上帳票を出す処理があった。元は先輩が書いた UNION ALL 4連結の SQL で、部署小計・支店小計・地域小計・総計を1つずつ足してた。読むのがしんどいし、区分が1つ増えるたびにブロックを足す羽目になる。

これを GROUP BY ROLLUP(地域, 支店, 部署) の1本に書き換えたら、60行あった SQL が15行くらいまで、いい感じに縮んだ。しかも実行計画を見たら、元は同じテーブルを4回読んでたのが1回に。体感でもちゃんと速くなりました。

詰まったのは、まさにこの記事の GROUPING() のところ。最初、小計行を WHERE 部署 IS NULL で拾おうとして、部署が未設定の実データが混ざって数字が合わなくなった。半日「なんで総計がズレるんだ」と悩んで、原因が「小計の NULL と実データの NULL がぶつかってる」だと気づいた時は、ちょっと脱力しましたね。GROUPING() を知ってれば5分で済む話だったんですよ。

だから声を大にして言いたい。ROLLUP を使うなら GROUPING() はセット。これだけ先に覚えておくと、俺みたいに時間を溶かさずに済みます!!

まとめ

SQL Server の ROLLUP / GROUPING SETS を、積み上げで見てきました。

  • ROLLUPGROUP BY に小計・総計を階層的に足す。ROLLUP(A, B) は右から列を畳む
  • 小計・総計行は畳まれた列が NULL になり、データ本来の NULL と見分けがつかない
  • 見分けるのが GROUPING(列)。畳まれた行なら 1。これでラベル付け・抽出をする
  • GROUPING SETS は欲しい集計の組み合わせを列挙する。() は総計。ROLLUP はその階層版ショートカット
  • UNION ALL の自前積み上げより、スキャン1回で短く書ける

帳票の「小計と総計を同じ結果に」という定番の注文は、ROLLUP と GROUPING() を押さえておけば SQL 一発で返せます。UNION ALL を積んで消耗してた世界が、列を1つ足すだけの世界になる。ここが ROLLUP を知っておく一番の効きどころです。

よくある質問

GROUPING() と GROUPING_ID() は何が違いますか

GROUPING(列) は1つの列について「畳まれた行か」を 1 / 0 で返します。GROUPING_ID(列1, 列2, ...) は複数の列の GROUPING をまとめてビットマスクの整数で返します。列が2〜3個までなら GROUPING() の組み合わせで CASE 分岐するのが読みやすく、列がもっと多くて集計レベルを数値で一括判定したい時に GROUPING_ID() が効いてきます。

ROLLUP の結果の並び順は保証されますか

保証されません。ROLLUP を付けても行の出力順は不定なので、帳票として並べたいなら忘れず ORDER BY を付けてください。小計・総計を各グループの下に配置したい場合は、ORDER BY GROUPING(部署), 部署, GROUPING(商品), 商品 のように GROUPING() を並び順の先頭に混ぜると、明細→小計→総計の順に整います。

WITH ROLLUP という書き方も見ますが、違いますか

GROUP BY 列1, 列2 WITH ROLLUP は古い(SQL Server 独自の)書き方で、GROUP BY ROLLUP(列1, 列2) の関数形式が ISO 標準です。挙動は基本的に同じですが、新しく書くなら関数形式を推奨します。GROUPING SETS と書式が揃い、部分的な小計指定にも自然に発展できるからです。

動作確認メモ

この記事の SQL(サンプルテーブル作成・ROLLUP・GROUPING() ラベル付け・GROUPING SETS)は、コンテナ上の SQL Server で実際に作成・実行し、記載の結果を確認しています。一時テーブル #Sales を最初に作る前提なので、各 SELECT を試す時は先頭の CREATE TABLE #Sales ブロックを1回流してから実行してください。

次に読むべき記事

以上!

同じ「小計を UNION ALL で積んで消耗してた」経験がある人、どんどんシェア待ってるぜ!!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


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

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

運営者について