Using GROUPING_ID to Distinguish Aggregation Levels in SELECT Statement

When working with ROLLUP or CUBE, you’ll notice NULL values appear in subtotals and grand totals.
But how do you tell the difference between a real NULL value in the data and a NULL that just means “subtotal here”?

That’s where GROUPING_ID comes in. It returns a numeric code indicating which columns are aggregated (rolled up).

Example 1: Distinguishing Department and Job Aggregations in Salary Totals

We calculate total salaries grouped by department and job. Using GROUPING_ID, we mark which rows are:

  • Detail rows
  • Department totals
  • Job totals
  • Grand total
SELECT department_id, job_id, SUM(salary) AS total_salary,
       GROUPING_ID(department_id, job_id) AS grouping_level
FROM employees
GROUP BY ROLLUP (department_id, job_id);

Output:

DEPARTMENT_ID JOB_ID     TOTAL_SALARY GROUPING_LEVEL
------------- ---------- ------------ --------------
           10 DEV                11000              0
           10 HR                  4000              0
           10                    15000              1
           20 DEV                 7000              0
           20 HR                  4500              0
           20                    11500              1
              ADM                 3000              2
                                 29500              3

GROUPING_LEVEL values:

  • 0 → Detail row (both dept and job present)
  • 1 → Department subtotal
  • 2 → Job subtotal
  • 3 → Grand total

Example 2: Identifying Aggregation Levels in Employee Count by Manager and Department

Here we count employees by manager and department, with ROLLUP.

SELECT manager_id, department_id, COUNT(employee_id) AS employee_count,
       GROUPING_ID(manager_id, department_id) AS grouping_level
FROM employees
GROUP BY ROLLUP (manager_id, department_id);

Output:

MANAGER_ID DEPARTMENT_ID EMPLOYEE_COUNT GROUPING_LEVEL
---------- ------------- -------------- --------------
       100            10              3              0
       100                            3              1
       101            20              2              0
       101                            2              1
                      10              3              2
                      20              2              2
                                      5              3

The codes show clearly whether the row is detail (0), subtotal by manager (1), subtotal by department (2), or grand total (3).

Example 3: Summing and Averaging Salaries by Department, Job, and Location Using GROUPING_ID

With CUBE, we calculate salary totals and averages by location, department, and job. GROUPING_ID tells us which level of aggregation each row represents.

SELECT d.location_id, e.department_id, e.job_id,
       SUM(e.salary) AS total_salary, AVG(e.salary) AS avg_salary,
       GROUPING_ID(d.location_id, e.department_id, e.job_id) AS grouping_level
FROM employees e
JOIN departments d ON e.department_id = d.department_id
GROUP BY CUBE (d.location_id, e.department_id, e.job_id);

Output (excerpt):

LOCATION_ID DEPARTMENT_ID JOB_ID     TOTAL_SALARY AVG_SALARY GROUPING_LEVEL
----------- ------------- ---------- ------------ ---------- --------------
       1000            10 DEV                11000       5500              0
       1000            10 HR                  4000       4000              0
       1000            10                    15000       5000              1
       2000            20 DEV                 7000       7000              0
       2000            20 HR                  4500       4500              0
       2000            20                    11500       5750              1
       1000                                  15000       5000              3
                          DEV                18000       6000              6
                                             29500       4916.67           7

GROUPING_ID helps identify whether the row is full detail (0), department subtotal (1), location subtotal (3), job subtotal (6), or grand total (7).

Example 4: Analyzing Job History by Aggregation Levels

Now let’s check job changes by employee and department, with CUBE.

SELECT e.employee_id, j.department_id, COUNT(*) AS job_changes,
       GROUPING_ID(e.employee_id, j.department_id) AS grouping_level
FROM job_history j
JOIN employees e ON j.employee_id = e.employee_id
GROUP BY CUBE (e.employee_id, j.department_id);

Output:

EMPLOYEE_ID DEPARTMENT_ID JOB_CHANGES GROUPING_LEVEL
----------- ------------- ----------- --------------
          1            10           1              0
          1            20           1              0
          1                         2              1
          2            10           1              0
          2                         1              1
          3            10           1              0
          3            20           1              0
          3                         2              1
          4            20           2              0
          4                         2              1
          6            10           1              0
          6                         1              1
                       10           4              2
                       20           4              2
                                    8              3

With GROUPING_ID, it’s clear which rows are employee-level totals, department totals, or overall totals.

Example 5: Salary Analysis by Job and Department Using GROUPING_ID

Finally, we compute both average and total salary with department/job roll-ups.

SELECT job_id, department_id, AVG(salary) AS avg_salary, SUM(salary) AS total_salary,
       GROUPING_ID(job_id, department_id) AS grouping_level
FROM employees
GROUP BY ROLLUP (job_id, department_id);

Output:

JOB_ID     DEPARTMENT_ID AVG_SALARY TOTAL_SALARY GROUPING_LEVEL
---------- ------------- ---------- ------------ --------------
DEV                   10       5500        11000              0
DEV                   20       7000         7000              0
DEV                            6000        18000              1
HR                    10       4000         4000              0
HR                    20       4500         4500              0
HR                             4250         8500              1
ADM                            3000         3000              1
                               4916.67     29500              3

Now you can distinguish between job/department detail (0), job totals (1), and the grand total (3).

Key Takeaways

  • GROUPING_ID provides a simple numeric way to detect subtotals vs detail rows.
  • Without it, NULL values can be confusing in roll-up/cube queries.
  • It’s perfect for reporting and BI dashboards, where you need to label rows as “Detail”, “Subtotal”, or “Grand Total”.

With GROUPING_ID, your reports become not just numbers, but structured insights — clearly showing where detail ends and totals begin.

Oracle DBA

Experienced OCM-certified Oracle Database Administrator with over 18 years of expertise in designing, implementing, and managing complex database solutions. My expertise spans performance optimization, security, and high-stakes solution implementation. Adept at managing complex environments with precision.

No Comments

    Leave a Message

    Your email address will not be published. All fields are mandatory. **