ROLLUP
El modificador WITH ROLLUP se añade a GROUP BY para generar automáticamente filas con subtotales y un total general. Es extremadamente útil para informes donde necesitas ver tanto el desglose detallado como los totales, sin tener que ejecutar consultas adicionales o calcularlos manualmente.
Sintaxis
SELECT columnas, funcion_agregacion(columna)
FROM tabla
GROUP BY columna1, columna2 WITH ROLLUP;WITH ROLLUP se escribe al final de la cláusula GROUP BY. MySQL genera filas adicionales con los subtotales de cada nivel de agrupación y una fila final con el total general.
ROLLUP básico
Total de pedidos e ingresos por estado, con un total general al final:
SELECT
estado,
COUNT(*) AS total_pedidos,
SUM(total) AS ingresos
FROM pedidos
GROUP BY estado WITH ROLLUP;| estado | total_pedidos | ingresos |
|---|---|---|
| cancelado | 3 | 898.98 |
| entregado | 7 | 4874.88 |
| enviado | 4 | 2038.96 |
| pendiente | 7 | 2153.91 |
| procesando | 4 | 1283.95 |
| NULL | 25 | 11250.68 |
Las primeras 5 filas son los grupos normales (uno por cada estado). La última fila, con NULL en la columna estado, es el total general que ROLLUP genera automáticamente: 25 pedidos en total con 11250.68 euros de ingresos.
El NULL en la columna de agrupación indica "todos los valores". Es la forma que tiene MySQL de decir que esa fila representa la suma de todos los grupos.
Mejorar la presentación con IFNULL
El NULL del total general no es muy legible. Puedes usar IFNULL o COALESCE para reemplazarlo:
SELECT
IFNULL(estado, 'TOTAL') AS estado,
COUNT(*) AS total_pedidos,
SUM(total) AS ingresos
FROM pedidos
GROUP BY estado WITH ROLLUP;| estado | total_pedidos | ingresos |
|---|---|---|
| cancelado | 3 | 898.98 |
| entregado | 7 | 4874.88 |
| enviado | 4 | 2038.96 |
| pendiente | 7 | 2153.91 |
| procesando | 4 | 1283.95 |
| TOTAL | 25 | 11250.68 |
Ahora la última fila muestra "TOTAL" en lugar de NULL.
ROLLUP con múltiples columnas
Con varias columnas en el GROUP BY, ROLLUP genera subtotales jerárquicos. Primero agrupa por todas las columnas, después genera subtotales quitando columnas de derecha a izquierda, y finalmente el total general:
SELECT
IFNULL(estado, '>> TOTAL') AS estado,
IFNULL(CAST(empleado_id AS CHAR), '> Subtotal') AS empleado,
COUNT(*) AS pedidos,
SUM(total) AS ingresos
FROM pedidos
GROUP BY estado, empleado_id WITH ROLLUP;| estado | empleado | pedidos | ingresos |
|---|---|---|---|
| cancelado | 4 | 1 | 699.00 |
| cancelado | 5 | 1 | 49.99 |
| cancelado | 6 | 1 | 149.99 |
| cancelado | > Subtotal | 3 | 898.98 |
| entregado | 4 | 3 | 3249.97 |
| entregado | 5 | 3 | 1109.95 |
| entregado | 6 | 1 | 449.98 |
| entregado | > Subtotal | 7 | 4809.90 |
| enviado | 4 | 2 | 1649.99 |
| enviado | 5 | 1 | 129.99 |
| enviado | 6 | 1 | 259.98 |
| enviado | > Subtotal | 4 | 2039.96 |
| pendiente | 4 | 2 | 99.98 |
| pendiente | 5 | 3 | 1643.97 |
| pendiente | 6 | 2 | 389.97 |
| pendiente | > Subtotal | 7 | 2133.92 |
| procesando | 4 | 1 | 599.99 |
| procesando | 5 | 1 | 199.99 |
| procesando | 6 | 2 | 483.97 |
| procesando | > Subtotal | 4 | 1283.95 |
| >> TOTAL | > Subtotal | 25 | 11166.71 |
Para cada estado, hay filas de detalle por empleado y una fila de subtotal (> Subtotal en la columna empleado). Al final, hay un total general (>> TOTAL). Esto da una vista completa tipo informe con tres niveles de agregación.
La función GROUPING()
El truco con IFNULL tiene un problema: ¿qué pasa si la columna ya contiene valores NULL legítimos? No podrías distinguir un NULL real de un NULL generado por ROLLUP.
MySQL 8.0.1 introdujo la función GROUPING() para resolver esto. Devuelve 1 si el valor NULL fue generado por ROLLUP, y 0 si es un valor normal:
SELECT
IF(GROUPING(estado), 'TOTAL', estado) AS estado,
COUNT(*) AS pedidos,
SUM(total) AS ingresos
FROM pedidos
GROUP BY estado WITH ROLLUP;| estado | pedidos | ingresos |
|---|---|---|
| cancelado | 3 | 898.98 |
| entregado | 7 | 4874.88 |
| enviado | 4 | 2038.96 |
| pendiente | 7 | 2153.91 |
| procesando | 4 | 1283.95 |
| TOTAL | 25 | 11250.68 |
GROUPING(estado) devuelve 1 para la fila de ROLLUP y 0 para las filas normales. IF(GROUPING(estado), 'TOTAL', estado) sustituye el valor solo en la fila de ROLLUP, dejando intactos los demás valores, incluyendo cualquier NULL legítimo.
ROLLUP para informes mensuales
Un caso de uso muy habitual es generar informes con subtotales por periodo:
SELECT
IF(GROUPING(anio), 'TOTAL', anio) AS anio,
IF(GROUPING(mes), 'Subtotal', mes) AS mes,
pedidos,
ingresos
FROM (
SELECT
YEAR(fecha_pedido) AS anio,
MONTH(fecha_pedido) AS mes,
COUNT(*) AS pedidos,
ROUND(SUM(total), 2) AS ingresos
FROM pedidos
GROUP BY YEAR(fecha_pedido), MONTH(fecha_pedido) WITH ROLLUP
) AS informe;| anio | mes | pedidos | ingresos |
|---|---|---|---|
| 2025 | 10 | 4 | 2794.92 |
| 2025 | 11 | 7 | 4553.89 |
| 2025 | 12 | 11 | 3901.87 |
| 2025 | Subtotal | 22 | 11250.68 |
| 2026 | 1 | 3 | 244.96 |
| 2026 | Subtotal | 3 | 244.96 |
| TOTAL | Subtotal | 25 | 11495.64 |
El informe muestra el desglose mensual, un subtotal por año y un total general. Es exactamente el tipo de salida que esperarías en un informe financiero.
ROLLUP y ORDER BY
Un detalle importante: no puedes usar ORDER BY para reordenar las filas cuando usas WITH ROLLUP si quieres que los subtotales mantengan su posición lógica. MySQL coloca las filas de ROLLUP justo después de las filas de su grupo, y un ORDER BY puede romper esa estructura.
Si necesitas ordenar y mantener los subtotales en su lugar, la recomendación es confiar en el orden natural que produce ROLLUP o usar la función GROUPING() como parte del ORDER BY:
SELECT
IF(GROUPING(estado), 'TOTAL', estado) AS estado,
COUNT(*) AS pedidos
FROM pedidos
GROUP BY estado WITH ROLLUP
ORDER BY GROUPING(estado), estado;Esto ordena primero los grupos normales (GROUPING = 0) alfabéticamente, y después la fila de total (GROUPING = 1) al final.
ROLLUP vs UNION ALL
Antes de que existiera ROLLUP, la forma de obtener subtotales era ejecutar la consulta principal y unirla con una consulta de totales:
-- Sin ROLLUP (más código, mismo resultado)
SELECT estado, COUNT(*) AS pedidos, SUM(total) AS ingresos
FROM pedidos
GROUP BY estado
UNION ALL
SELECT 'TOTAL', COUNT(*), SUM(total)
FROM pedidos;WITH ROLLUP hace esto automáticamente y de forma más eficiente, ya que MySQL solo necesita recorrer los datos una vez.
Con esto completamos la sección de agrupación. En la siguiente sección exploraremos los diferentes tipos de JOIN para combinar datos de múltiples tablas.
Escrito por Eduardo Lázaro
