存储过程慢的首要原因是未命中过程缓存,mysql 5.7.21/8.0.3起才支持ps_cache,低版本每次call均重新解析优化;其次参数类型不匹配导致隐式转换使索引失效,以及循环内重复查询引发n+1问题。

存储过程慢,先查是不是根本没走过程缓存
MySQL 5.7.21 之前版本(比如你还在用 5.6 或早期 5.7),CALL 每次都会重新解析、优化、生成执行计划——不是“慢”,是“每次都重来”。这不是写法问题,是版本限制。
验证方法很简单:SELECT VERSION();。如果低于 5.7.21,别在参数、索引上反复折腾,升级才是根治方案。
即使版本达标,也要确认实际是否命中缓存:查 performance_schema.events_statements_history_long,看同名 CALL my_proc() 多次调用时,PLAN_EXPLAIN 字段是否重复出现;如果每次都不一样,说明缓存未生效,大概率是参数类型不一致触发了隐式转换。
参数类型不匹配会让索引彻底失效
声明 IN p_id INT,但调用时传入字符串 CALL my_proc('123'),MySQL 会把整列转成数字比对,导致 WHERE id = p_id 无法走索引——这种错误在存储过程里极难察觉,因为语法完全合法。
- 参数类型必须和目标字段**完全一致**:INT 对 INT,VARCHAR(32) 对 VARCHAR(32),字符集也得一样
- 调试时加一句
SELECT p_id, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='t' AND COLUMN_NAME='id';对比类型 - 宁可显式
CAST(p_id AS SIGNED),也不要依赖自动转换
循环里查数据库 = 自己造 N+1
WHILE 遍历 ID 列表,每次循环都 SELECT * FROM orders WHERE user_id = p_uid,这比应用层 N+1 更糟:每次查询都要走完整优化流程 + 网络协议打包 + 结果集构造,上下文开销翻倍。
正确做法是把逻辑“拉平”:
- 用临时表装 ID 列表:
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY);,再INSERT INTO tmp_ids SELECT ... - 然后一次 JOIN:
SELECT o.* FROM orders o JOIN tmp_ids t ON o.user_id = t.id; - 临时表记得加主键或索引,否则 JOIN 效率比全表扫描还低
裸 SELECT 是隐形内存炸弹
SELECT status FROM users WHERE id = p_id; 这句没接 INTO @var,也没包在 INSERT SELECT 或游标里?MySQL 会强制构造结果集、分配缓冲区、走网络协议栈——哪怕你什么都没读,它已经干完了所有事。
这类语句在调试时随手加的,上线后就是性能黑洞:
- 所有中间查询必须有明确目的:取值就
SELECT ... INTO,写入就INSERT SELECT,判断存在性就SELECT COUNT(*)+IF - EXPLAIN 只能对外部
CALL语句做分析,真正要盯的是过程体里每条 SQL 单独跑EXPLAIN FORMAT=TREE - 重点看
type是否为ref或更高,key是否非 NULL,rows是否接近实际匹配行数
最常被忽略的点:慢的从来不是存储过程本身,而是里面那条没走索引的 SELECT,或者返回了 10 万行只用了第一行的裸查询。











