GROUP BY 配合 SUM() 是分组求和的标准高效解法,需正确使用 WHERE 过滤、显式指定分组字段、避免 SELECT *,并确保 GROUP BY 字段有索引。

GROUP BY 配合 SUM() 是最直接的解法
分组去重本身不是 SQL 的原生操作,真正要的是「按某几列分组后,对数值列求和」——这等价于把重复键对应的值合并累加。用 GROUP BY + SUM() 就是标准且最高效的方式,数据库优化器对这种组合有成熟执行计划支持。
常见错误是先用 DISTINCT 再套 SUM(),比如 SELECT SUM(col) FROM (SELECT DISTINCT key, col FROM t),这不仅逻辑错(丢掉了分组维度),还强制物化中间结果,性能差得多。
- 必须把分组字段全写进
GROUP BY,不能只写主键或部分字段,否则 MySQL 8.0+ 会报错(ONLY_FULL_GROUP_BY模式下) - 如果某行
col是NULL,SUM()自动忽略,不参与计算,无需额外COALESCE - 聚合前加
WHERE过滤比聚合后加HAVING快,因为能减少扫描行数
遇到重复键但需保留非聚合字段时,用 ANY_VALUE() 或子查询
当 SELECT 列里除了分组字段和 SUM() 外,还想带一个“任意一条记录的 status”这类非聚合字段,MySQL 会报错:「Expression #3 of SELECT list is not in GROUP BY clause」。这时候不能靠 DISTINCT 解决,得明确语义。
ANY_VALUE(status) 是最轻量的选择,它告诉优化器“这个字段值在组内都一样,或你随便挑一个就行”,不会触发排序或临时表。比用 MAX(status) 或子查询更高效。
- 仅限 MySQL 5.7+,且确认业务上该字段在同组内确实无实质差异,否则结果不可控
- PostgreSQL 要用
MIN(status)或窗口函数配合DISTINCT ON,不能直接用ANY_VALUE - 如果字段有业务含义(如最新时间戳),就别用
ANY_VALUE,改用JOIN子查询或窗口函数
WHERE 条件写错位置会导致 SUM 结果异常
很多人把过滤条件放在 HAVING 里,以为和 WHERE 一样,其实 HAVING 是在分组、聚合之后才执行,意味着数据库已经算完了所有组的 SUM(),再扔掉不满足条件的组——白算了很多。
例如想统计「每个用户近30天的订单金额总和」,错误写法:HAVING order_time > NOW() - INTERVAL 30 DAY;正确写法必须是 WHERE order_time > NOW() - INTERVAL 30 DAY 放在 GROUP BY 前。
-
WHERE过滤行,HAVING过滤组,二者阶段不同,不可互换 - 索引是否生效,取决于
WHERE中的字段是否有对应索引,HAVING中的条件无法走索引 - 如果过滤依赖聚合结果(如「总金额 > 1000 的用户」),才必须用
HAVING
大数据量下避免 SELECT * + GROUP BY
写 SELECT *, SUM(amount) FROM t GROUP BY user_id 看似方便,实际是性能杀手:数据库必须把每行所有字段都拉进来参与分组,即使只关心 2–3 个字段,也会读取并传输大量冗余数据。
尤其当表有 TEXT、JSON 或宽字段时,内存占用和网络开销剧增,还可能触发磁盘临时表(Using temporary; Using filesort)。
- 显式列出需要的字段:
SELECT user_id, ANY_VALUE(name), SUM(amount) FROM t GROUP BY user_id - 确保
GROUP BY字段上有索引,最好是联合索引覆盖查询字段(如(user_id, name, amount)) - 如果只是做报表导出,考虑加
SQL_BIG_RESULT提示,让优化器倾向用磁盘临时表而非内存,避免 OOM
实际最易被忽略的是索引设计——GROUP BY 字段没索引时,即使语句写得再规范,也会退化成全表扫描+文件排序。别只盯着写法,先看 EXPLAIN 里的 type 是不是 ref 或 range。

















