Oracle では GROUP BY を拡張した ROLLUP・CUBE・GROUPING SETS を使うと、小計・合計・多次元集計を複数の SQL に分けることなく1文で実現できます。
たとえば「部門別・月別の売上合計 + 各部門の年計 + 全社合計」を通常の GROUP BY で取得しようとすると複数の SELECT を UNION ALL でつなぐ必要がありますが、ROLLUP を使えば1文で書けます。
- ROLLUP で階層的な小計・合計行を自動生成する方法
- CUBE で全軸の組み合わせ集計を一括取得する方法
- GROUPING SETS で必要な集計軸だけを指定する方法
- GROUPING() 関数で小計行と NULL 値を区別するテクニック
- GROUPING_ID() で複数列の集計レベルをまとめて判定する方法
- UNION ALL との比較・どちらを使うべきかの判断基準
- 月別売上・部門別実績レポートの実務パターン
GROUP BY だけでは小計・合計を一発で取れない
通常の GROUP BY では「部門ごとの集計」または「全社合計」のどちらか一方しか取れません。小計行も含めてレポート形式で取得するには、UNION ALL で複数 SELECT を重ねる必要があり、冗長になります。
-- 部門別・月別の売上 + 部門合計 + 全社合計を取得する(UNION ALL 方式) -- 同じテーブルを3回スキャンする → パフォーマンスが悪い SELECT department_id, TO_CHAR(sale_date, 'MM') AS month, SUM(amount) AS total FROM sales GROUP BY department_id, TO_CHAR(sale_date, 'MM') UNION ALL SELECT department_id, NULL AS month, SUM(amount) AS dept_total FROM sales GROUP BY department_id UNION ALL SELECT NULL AS department_id, NULL AS month, SUM(amount) AS grand_total FROM sales; -- ROLLUP を使えば1文で同じ結果が得られる(テーブルスキャン1回)
ROLLUP:階層的な小計・合計を自動生成する
GROUP BY ROLLUP(列1, 列2) は、右端の列から順に集計列を取り除いた小計行を自動的に追加します。N個の列を指定すると N+1 レベルの集計(詳細行 + 各段階の小計 + 総計)が生成されます。
-- GROUP BY ROLLUP(department_id, month_no) で以下の3レベルが生成される
-- ① department_id + month_no ごとの集計(明細行)
-- ② department_id ごとの小計(month_no は NULL)
-- ③ 総計(department_id も month_no も NULL)
SELECT
department_id,
TO_CHAR(sale_date, 'MM') AS month,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM sales
GROUP BY ROLLUP(department_id, TO_CHAR(sale_date, 'MM'))
ORDER BY department_id NULLS LAST, month NULLS LAST;
-- 結果イメージ:
-- DEPT MONTH TOTAL_AMOUNT
-- 10 01 500000
-- 10 02 620000
-- 10 NULL 1120000 ← 部門10の小計
-- 20 01 430000
-- 20 02 380000
-- 20 NULL 810000 ← 部門20の小計
-- NULL NULL 1930000 ← 総計
ROLLUP が生成した小計・総計行では、集計に使われなかった列の値が
NULL になります。ただし、元データにも NULL がある場合は区別がつかないため、GROUPING() 関数を使って判別します(後述)。
-- 年は固定し、部門×月の小計だけ生成する(年の総計行は不要)
SELECT
TO_CHAR(sale_date, 'YYYY') AS year,
department_id,
TO_CHAR(sale_date, 'MM') AS month,
SUM(amount) AS total
FROM sales
WHERE TO_CHAR(sale_date, 'YYYY') = '2025'
GROUP BY TO_CHAR(sale_date, 'YYYY'), -- ROLLUP の外: 常にグループ化
ROLLUP(department_id, TO_CHAR(sale_date, 'MM')) -- ここだけ小計生成
ORDER BY department_id NULLS LAST, month NULLS LAST;
GROUPING() 関数:小計行と NULL 値を区別する
GROUPING(列名) は、その列が ROLLUP/CUBE によって集計されている(小計・総計行)場合に 1、通常のグループ行の場合に 0 を返します。元データの NULL と ROLLUP が生成した NULL を区別するために使います。
SELECT
CASE GROUPING(department_id)
WHEN 1 THEN '【全部門合計】'
ELSE TO_CHAR(department_id)
END AS dept_label,
CASE GROUPING(TO_CHAR(sale_date, 'MM'))
WHEN 1 THEN '【月計】'
ELSE TO_CHAR(sale_date, 'MM') || '月'
END AS month_label,
SUM(amount) AS total_amount,
GROUPING(department_id) AS g_dept, -- デバッグ用
GROUPING(TO_CHAR(sale_date, 'MM')) AS g_month
FROM sales
GROUP BY ROLLUP(department_id, TO_CHAR(sale_date, 'MM'))
ORDER BY department_id NULLS LAST, TO_CHAR(sale_date, 'MM') NULLS LAST;
-- 結果イメージ:
-- DEPT_LABEL MONTH_LABEL TOTAL_AMOUNT G_DEPT G_MONTH
-- 10 01月 500000 0 0
-- 10 02月 620000 0 0
-- 10 【月計】 1120000 0 1
-- 20 01月 430000 0 0
-- 20 02月 380000 0 0
-- 20 【月計】 810000 0 1
-- 【全部門合計】【月計】 1930000 1 1
CUBE:全軸の組み合わせを一括集計する
GROUP BY CUBE(列1, 列2) は、指定した列のすべての組み合わせについて集計行を生成します。N個の列を指定すると 2^N 通りの組み合わせが生成されます。多次元分析(クロス集計)に使います。
-- GROUP BY CUBE(region, category) で以下の4通りが生成される
-- ① region + category(明細行)
-- ② region 別合計(category は NULL)
-- ③ category 別合計(region は NULL)
-- ④ 総計(region も category も NULL)
SELECT
NVL(TO_CHAR(region), '【全地域】') AS region_label,
NVL(category, '【全カテゴリ】') AS category_label,
SUM(sales_amount) AS total,
COUNT(DISTINCT customer_id) AS unique_customers,
GROUPING(region) AS g_region,
GROUPING(category) AS g_category
FROM sales_data
GROUP BY CUBE(region, category)
ORDER BY g_region, g_category, region, category;
-- 2列 CUBE で生成される行パターン:
-- g_region=0, g_category=0: 地域×カテゴリの明細
-- g_region=0, g_category=1: 地域別合計
-- g_region=1, g_category=0: カテゴリ別合計
-- g_region=1, g_category=1: 総計
3列の CUBE は 2³ = 8 通り、4列は 2⁴ = 16 通りの集計行が生成されます。必要な集計軸が限られている場合は GROUPING SETS の使用を検討してください。
GROUPING SETS:必要な集計軸だけを指定する
GROUPING SETS は ROLLUP や CUBE のように自動生成するのではなく、必要な集計パターンを明示的に列挙します。「このパターンとあのパターンの組み合わせだけ欲しい」という場合に最適です。
-- (department_id, month) の組み合わせ + department_id 別合計 + 総計
-- ← ROLLUP(department_id, month) と同じ結果
SELECT
department_id,
TO_CHAR(sale_date, 'MM') AS month,
SUM(amount) AS total
FROM sales
GROUP BY GROUPING SETS (
(department_id, TO_CHAR(sale_date, 'MM')), -- 部門×月
(department_id), -- 部門合計
() -- 総計(空のタプル)
)
ORDER BY department_id NULLS LAST, month NULLS LAST;
-- 「部門×月」と「商品×月」の2種類の集計を1文で取得する(ROLLUP では作れない)
SELECT
department_id,
product_id,
TO_CHAR(sale_date, 'MM') AS month,
SUM(amount) AS total,
GROUPING(department_id) AS g_dept,
GROUPING(product_id) AS g_prod
FROM sales
GROUP BY GROUPING SETS (
(department_id, TO_CHAR(sale_date, 'MM')), -- パターン①: 部門×月
(product_id, TO_CHAR(sale_date, 'MM')), -- パターン②: 商品×月
() -- 総計
)
ORDER BY g_dept, g_prod, month;
GROUPING_ID():複数列の集計レベルをビットで一括判定
GROUPING_ID(列1, 列2, ...) は各列の GROUPING() 結果を2進数で組み合わせた数値を返します。複数列の集計レベルを1つの数値で判定できるため、GROUPING() を列ごとに書くより簡潔になります。
-- GROUPING_ID(department_id, month) の値:
-- 0 (00): 部門×月の明細行
-- 1 (01): 部門合計行(月が集計)
-- 2 (10): 月別合計行(部門が集計)※ CUBE の場合
-- 3 (11): 総計行(両方集計)
SELECT
department_id,
TO_CHAR(sale_date, 'MM') AS month,
SUM(amount) AS total,
GROUPING_ID(department_id, TO_CHAR(sale_date, 'MM')) AS gid,
CASE GROUPING_ID(department_id, TO_CHAR(sale_date, 'MM'))
WHEN 0 THEN '明細'
WHEN 1 THEN '部門計'
WHEN 3 THEN '総計'
END AS row_type
FROM sales
GROUP BY ROLLUP(department_id, TO_CHAR(sale_date, 'MM'))
ORDER BY department_id NULLS LAST, month NULLS LAST;
ROLLUP / CUBE / GROUPING SETS / UNION ALL の比較
| 方法 | 生成される集計 | テーブルスキャン | 向いているケース |
|---|---|---|---|
| ROLLUP | 階層的な小計(右から順に省略) | 1回 | 売上レポートの合計・月計・年計 |
| CUBE | 全列の組み合わせ(2^N 通り) | 1回 | 多次元分析・クロス集計 |
| GROUPING SETS | 明示した集計パターンのみ | 1回 | 特定のパターンだけ必要な場合 |
| UNION ALL | 自由に定義可能 | クエリ数分 | 集計パターンが複雑で上記で書けない場合 |
実務パターン:月次売上レポートを ROLLUP で作成する
-- 実際の売上レポートで使える完成形パターン
SELECT
CASE GROUPING(d.department_name)
WHEN 1 THEN '★ 全社合計'
ELSE d.department_name
END AS dept_name,
CASE GROUPING(TO_CHAR(s.sale_date, 'MM'))
WHEN 1 THEN '小計'
ELSE TO_CHAR(s.sale_date, 'MM') || '月'
END AS month_label,
TO_CHAR(SUM(s.amount), '999,999,999') AS total_amount,
COUNT(*) AS order_count,
TO_CHAR(AVG(s.amount), '999,999') AS avg_amount
FROM sales s
JOIN departments d ON s.department_id = d.department_id
WHERE s.sale_date >= DATE '2025-01-01'
AND s.sale_date < DATE '2026-01-01'
GROUP BY ROLLUP(d.department_name, TO_CHAR(s.sale_date, 'MM'))
ORDER BY
GROUPING(d.department_name), -- 総計行を最後に
d.department_name NULLS LAST,
GROUPING(TO_CHAR(s.sale_date, 'MM')), -- 小計行を各部門の最後に
TO_CHAR(s.sale_date, 'MM') NULLS LAST;
まとめ
- ROLLUP:最もよく使う。階層的な小計・合計行を自動生成。売上レポートに最適
- CUBE:全軸の組み合わせを一括集計。多次元分析に使うが行数が増えやすい
- GROUPING SETS:必要な集計パターンを明示指定。柔軟で CUBE より効率的
GROUPING(列):小計行(=1)と通常行(=0)を区別。小計行のラベルを CASE で置き換えるGROUPING_ID():複数列の集計レベルを1つの数値で判定。CASE 文をシンプルにできる- UNION ALL よりテーブルスキャンが1回で済むためパフォーマンスが高い
ROLLUP/CUBE で生成した小計行を横に並べたい場合はOracle PIVOT演算子の使い方も参照してください。また、集計結果に対してランキングや累計を計算したい場合はOracle 分析関数(ウィンドウ関数)完全ガイドが役立ちます。

