存储过程group by慢主因是执行计划未优化:索引未覆盖group by和select字段、触发临时表与文件排序;应确保联合索引最左前缀覆盖聚合列,避免函数表达式,大中间结果用create temporary table并手动建索引,压平n+1查询,优先应用层聚合。

存储过程里用 GROUP BY 慢得离谱,是不是写法错了?
不是语法错,是执行计划没绕过临时表和文件排序。MySQL 存储过程中跑聚合,和直接在客户端发 SQL 一样走优化器,但容易忽略两点:索引是否覆盖所有 GROUP BY 和 SELECT 字段、是否被迫用到了 Using temporary; Using filesort。
实操建议:
- 用
EXPLAIN FORMAT=TRADITIONAL查看存储过程内每条查询的执行计划(注意:需在存储过程里加SELECT前临时开启profiling或改用information_schema.PROCESSLIST配合慢日志定位) -
GROUP BY字段必须是联合索引最左前缀,且索引要包含所有被聚合引用的列(比如SELECT dept_id, COUNT(*), AVG(salary) FROM emp GROUP BY dept_id,索引至少是(dept_id, salary)) - 避免在
GROUP BY表达式里用函数,如GROUP BY DATE(create_time)——会跳过索引,改成建生成列 + 索引
DECLARE 临时表 vs CREATE TEMPORARY TABLE,聚合中间结果该选哪个?
临时表更可控,DECLARE 的游标或变量只适合单值或极小集合;一旦要存几千行中间聚合结果,DECLARE 的 TABLE 变量(MySQL 8.0.19+ 支持)没索引、不走缓冲池、不能 ANALYZE,性能反不如物理临时表。
实操建议:
- 中间结果超 100 行就别用
DECLARE my_tmp TABLE,改用CREATE TEMPORARY TABLE tmp_agg (dept_id INT, cnt INT, PRIMARY KEY(dept_id)) - 临时表务必手动加索引(尤其是后续要
JOIN或WHERE的字段),MySQL 不会自动为临时表建统计信息 - 记得在存储过程末尾
DROP TEMPORARY TABLE IF EXISTS tmp_agg,否则连接复用时可能报Table 'tmp_agg' already exists
存储过程调用时 CPU 突增、锁等待超时,是不是聚合逻辑触发了全表扫描?
大概率是。存储过程本身不改变隔离级别,但聚合语句若没走索引,会在事务内长时间持有行锁或间隙锁;更麻烦的是,如果过程里嵌套循环查聚合(比如遍历部门再查每个部门销售额),就会变成 N+1 查询模式,放大锁竞争。
实操建议:
- 把多层循环聚合压平成单次大查询,例如用
WITH RECURSIVE(MySQL 8.0+)或预计算维度表替代游标逐行处理 - 聚合前加
SELECT ... FOR UPDATE SKIP LOCKED(仅适用可重复读级别),避免因锁等待拖垮整个过程 - 检查是否误用了
SELECT *再GROUP BY,这会导致 MySQL 无法使用松散索引扫描(Loose Index Scan),强制走紧凑扫描(Tight Index Scan)
为什么加了索引、也用了临时表,存储过程里跑聚合还是比应用层慢?
因为存储过程运行在服务端,但默认没开查询缓存(MySQL 8.0 已移除),且每次调用都重新解析、生成执行计划;而应用层如果用预编译 PreparedStatement,计划能复用,参数化也更干净。
实操建议:
- 确认 MySQL 是否启用了
query_cache_type=OFF(8.0 默认关),别指望它加速存储过程内的查询 - 聚合逻辑若不依赖过程内变量,优先拆到应用层做——特别是需要分页、动态条件或对接缓存的场景
- 真要留在存储过程里,把聚合部分抽成独立的
VIEW或内联表值函数(MySQL 不支持 TVF,可用视图模拟),让优化器更容易重用计划
复杂点在于:聚合字段是否参与排序、是否涉及 JSON_AGG 类函数、以及分区表下 GROUP BY 是否跨分区——这些都会让执行计划彻底变样,但错误往往只在 EXPLAIN 输出里多一行 Using MRR 或少一个 key_len 值。











