不能对同一张表反复扫描做多个GROUP BY,因每次扫描均需存储层I/O、加锁及执行计划解析,开销巨大;MySQL等引擎还限制同语句多次引用派生表,直接报错。

为什么不能对同一张表反复扫描做多个 GROUP BY
反复扫描会显著拖慢查询,尤其在大表或高并发场景下。数据库每次扫描都要走存储层、加锁、解析执行计划,开销远超内存中的一次计算。更关键的是,某些引擎(如 MySQL 5.7 的 MyISAM)甚至不支持同一语句中多次引用同一派生表,直接报错 ERROR 1248 (42000): Every derived table must have its own alias。
用单个 GROUP BY + 多个聚合函数实现并行计算
标准 SQL 允许在同一个 GROUP BY 子句后,写多个互不影响的聚合表达式。它们共享同一轮分组扫描,由优化器自动合并为一次哈希分组或排序分组。这是最轻量、兼容性最好的方式。
-
COUNT(*)、SUM(amount)、AVG(price)、MAX(created_at)可同时出现在SELECT中 - 注意:所有非聚合字段必须出现在
GROUP BY列表里,否则 PostgreSQL/MySQL 8.0+ 会报错ERROR 1140: In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column - 避免在聚合中混用条件逻辑却忘了用
CASE WHEN,例如统计“支付成功订单数”不能直接写COUNT(status = 'success')(这在 MySQL 中返回行数,不是布尔计数),得写成COUNT(CASE WHEN status = 'success' THEN 1 END)
SELECT region, COUNT(*) AS total_orders, COUNT(CASE WHEN status = 'success' THEN 1 END) AS success_cnt, SUM(amount) AS total_amount, AVG(amount) AS avg_amount, MIN(created_at) AS first_order FROM orders WHERE created_at >= '2024-01-01' GROUP BY region;
需要不同过滤条件时,用 FILTER(PostgreSQL)或 CASE WHEN(通用)
当各指标需独立 WHERE 条件(比如“近7天下单用户数”和“近30天复购用户数”),不能靠外层再套子查询——那又变两次扫描了。PostgreSQL 支持 FILTER 子句,语义清晰且仍单扫;其他数据库统一用 CASE WHEN 模拟。
- PostgreSQL 写法:
COUNT(*) FILTER (WHERE dt >= current_date - 7) - MySQL / SQL Server / Oracle 均需写:
COUNT(CASE WHEN dt >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 END) - 别写
SUM(IF(...))或SUM(CASE ... THEN 1 ELSE 0 END)——虽然结果对,但语义是“求和”,不如COUNT(... THEN 1 END)直观且部分优化器能更好识别空值行为
复杂指标(如分位数、去重计数)会影响单扫假设
COUNT(DISTINCT user_id) 和 PERCENTILE_CONT(0.5) 这类函数,在多数引擎中无法与普通聚合完全共享中间状态。例如 MySQL 5.7 对 COUNT(DISTINCT) 会单独建临时哈希表;Spark SQL 中 approx_count_distinct 虽快但仍是独立通道。这意味着:表面是一次 GROUP BY,底层可能触发多阶段 shuffle 或额外内存结构。
- 确认执行计划:MySQL 看
EXPLAIN FORMAT=TREE是否出现多个Aggregate节点;PostgreSQL 看EXPLAIN (ANALYZE)中是否有重复GroupAggregate或HashAggregate - 若必须用高精度去重,且数据量大,考虑预计算到汇总表,而非每次实时算
-
STRING_AGG、JSON_ARRAYAGG等集合拼接函数也属于“重量级聚合”,容易撑爆内存,慎放在多指标共查中
单次扫描能否真正覆盖所有聚合,取决于具体函数是否具备“可合并性”——不是语法允不允许写在一起,而是执行引擎有没有把它们编译进同一个聚合通道。这点很容易被 explain 输出里的“一个 Group By”误导。










