先聊聊ROLLUP。它其实是GROUP BY的一个扩展语法,专门用来干一件事——自动生成多级小计和总计行。它的工作方式是按列从左到右逐级上卷,比如`GROUP BY a,b,c WITH ROLLUP`,会生成`(a,b,c)`、`(a,b,NULL)`、`(a,NULL,NULL)`、`(NULL,NULL,NULL)`四级汇总,而普通GROUP BY只给你最细粒度的分组,想要小计?得自己写SQL。

ROLLUP 是什么,它和普通 GROUP BY 有什么区别?
`ROLLUP` 不是独立函数,而是 `GROUP BY` 的扩展语法,用于在分组结果中自动添加多级汇总行(小计、合计)。它按括号内列的**从左到右顺序**逐级上卷:先按所有列分组,再依次去掉最右边一列,直到只剩第一列,最后加一行全表总计。这里有个常见误区:很多人把它当成函数写成 `ROLLUP(column)` —— 实际上必须写成 `GROUP BY column1, column2 WITH ROLLUP`(MySQL)或 `GROUP BY ROLLUP(column1, column2)`(PostgreSQL / SQL Server / Oracle)。不同数据库的写法差异明显:
- MySQL:用 `WITH ROLLUP` 后缀,且不支持括号内嵌套
- PostgreSQL / SQL Server:用 `GROUP BY ROLLUP(column1, column2)`
- Oracle:同样支持 `ROLLUP`,但要注意空值处理逻辑
如何识别 ROLLUP 生成的汇总行?
`ROLLUP` 会让某一层级的分组列值变为 `NULL`,表示该行是上一级的汇总。比如 `GROUP BY ROLLUP(dept, role)` 中:
- `dept = 'eng'`, `role = 'dev'` → 正常明细行
- `dept = 'eng'`, `role = NULL` → 该部门小计
- `dept = NULL`, `role = NULL` → 全表合计
这里有个坑:不能简单靠 `IS NULL` 来判断它是不是汇总行——因为原始数据本身也可能有 `NULL`。更靠谱的方式是用 `GROUPING()` 函数(PostgreSQL/SQL Server/Oracle 支持),例如 `GROUPING(dept)` 返回 1 表示该行中 `dept` 是 `ROLLUP` 生成的占位 `NULL`;MySQL 没有 `GROUPING()`,只能靠结合业务逻辑和多列组合判空来规避歧义。
为什么 SUM() 和 COUNT() 在 ROLLUP 行里结果“看起来不对”?
`SUM()` 在汇总行表现正常,但 `COUNT(*)` 就容易踩坑了:它统计的是当前分组下的所有行数,包括 `ROLLUP` 插入的汇总行本身。比如部门小计行也会被 `COUNT(*)` 计入一次,导致数值虚高。更稳妥的做法是:
- 用 `COUNT(column)`(非空计数)替代 `COUNT(*)`,避免把 `NULL` 占位行算进去
- 或显式过滤:在 MySQL 中加 `HA VING dept IS NOT NULL OR role IS NOT NULL` 排除全 `NULL` 行(但会丢掉合计)
- 聚合前先用 `CASE WHEN GROUPING(dept) = 1 THEN 'Total' ELSE dept END` 做标签,比硬判 `NULL` 更安全
ROLLUP 性能和可读性要注意什么?
性能开销也不小。`ROLLUP` 本质是数据库内部多次分组再合并,数据量大时比普通 `GROUP BY` 多出约 2×~3× 的计算开销(取决于维度数)。3 列 `ROLLUP(a,b,c)` 实际执行相当于 `GROUP BY a,b,c` + `GROUP BY a,b` + `GROUP BY a` + 全表聚合。容易被忽略的一点是:排序不可控。汇总行插入位置由数据库决定(通常是末尾或对应层级),若需固定顺序(如合计总在最后),必须显式加 `ORDER BY`,且要用 `GROUPING()` 或构造排序键,例如:
ORDER BY GROUPING(dept), dept, GROUPING(role), role
多维 `ROLLUP` 还可能触发内存溢出(尤其在 MySQL 5.7 默认配置下),建议先在小数据集验证,再上线。