【Oracle】ROLLUP・CUBE・GROUPING SETS完全ガイド|小計・合計・多次元集計をSQLで一発実現する方法を実例で解説

【Oracle】ROLLUP・CUBE・GROUPING SETS完全ガイド|小計・合計・多次元集計をSQLで一発実現する方法を実例で解説 Oracle

Oracle では GROUP BY を拡張した ROLLUPCUBEGROUPING 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 を重ねる必要があり、冗長になります。

通常の GROUP BY + UNION ALL で小計・合計を作る(冗長な例)
-- 部門別・月別の売上 + 部門合計 + 全社合計を取得する(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 レベルの集計(詳細行 + 各段階の小計 + 総計)が生成されます。

ROLLUP の基本:部門別・月別の売上 + 部門計 + 総計を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   ← 総計
小計・総計行では集計対象外の列が NULL になる
ROLLUP が生成した小計・総計行では、集計に使われなかった列の値が NULL になります。ただし、元データにも NULL がある場合は区別がつかないため、GROUPING() 関数を使って判別します(後述)。
部分 ROLLUP:列の一部だけを ROLLUP の対象にする
-- 年は固定し、部門×月の小計だけ生成する(年の総計行は不要)
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 を区別するために使います。

GROUPING() で小計行の 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 通りの組み合わせが生成されます。多次元分析(クロス集計)に使います。

CUBE の基本:地域×商品カテゴリの多次元集計
-- 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: 総計
CUBE は列数が増えると行数が爆発的に増える
3列の CUBE は 2³ = 8 通り、4列は 2⁴ = 16 通りの集計行が生成されます。必要な集計軸が限られている場合は GROUPING SETS の使用を検討してください。

GROUPING SETS:必要な集計軸だけを指定する

GROUPING SETS は ROLLUP や CUBE のように自動生成するのではなく、必要な集計パターンを明示的に列挙します。「このパターンとあのパターンの組み合わせだけ欲しい」という場合に最適です。

GROUPING SETS の基本:必要な集計パターンだけを指定
-- (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;
GROUPING SETS 応用:ROLLUP では作れないパターン
-- 「部門×月」と「商品×月」の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() で集計レベルを一括判定する
-- 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 分析関数(ウィンドウ関数)完全ガイドが役立ちます。