mysql 5.7.21及8.0.3起才支持存储过程执行计划缓存(ps_cache),此前版本每次call均重新解析优化;需用select version()确认版本,低于5.7.21则无此缓存。

确认 MySQL 版本是否支持过程级执行计划缓存
MySQL 5.7.21 之前版本(包括全部 5.6 和早期 5.7)根本不支持存储过程的执行计划缓存(ps_cache)。每次 CALL my_proc() 都会重新解析、重写、优化并生成新执行计划——这是最隐蔽也最严重的性能瓶颈。
验证方式:SELECT VERSION();,若结果低于 5.7.21,就别在过程体里花时间调优 SQL,先升级或重构为应用层逻辑。
即使版本达标,也要注意:该缓存仅对同一连接内重复调用生效;跨连接、重启服务、或修改过过程定义后都会失效。不能依赖它替代 SQL 本身的索引与结构优化。
参数类型必须与表字段完全一致,否则隐式转换让索引失效
声明 IN p_id INT,但调用时传入 CALL my_proc('123'),MySQL 会把整列 id 转成字符串做比较,导致全表扫描——这种错误在过程里极难被 EXPLAIN 捕获,因为参数值在运行时才代入。
- 检查字段真实类型:
SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='t' AND COLUMN_NAME='id'; - 过程内加诊断语句:
SELECT p_id, 'p_id type is INT?' AS check_hint;,配合客户端看实际传入值 - 强制统一类型:用
CAST(p_id AS UNSIGNED)或直接声明为DECIMAL(20,0)更安全(尤其对接 Java/PHP 的长整型)
禁止在 WHILE/REPEAT 循环中执行 SELECT 或 DML
“遍历 ID 列表 → 对每个 ID 查一次” 是典型 N+1 反模式。在存储过程中尤其危险:每次循环都触发独立查询解析 + 优化 + 执行,开销远高于应用层。
正确做法是把循环逻辑上推为集合操作:
- 用临时表承载 ID 集合:
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY);,插入后立刻加主键或唯一索引 - 改写为单次 JOIN:
SELECT t.* FROM t JOIN tmp_ids ON t.id = tmp_ids.id; - 批量更新/删除同理:
UPDATE t JOIN tmp_ids USING (id) SET ...; - 若 ID 来源是字符串拼接,用
FIND_IN_SET(id, p_id_list)仅限小数据量;大列表务必走临时表
DELIMITER 和错误码覆盖不全会导致过程创建失败或静默吞错
DELIMITER 不是可选语法糖,而是客户端切分多语句的开关。漏写或写错(比如写成 DELIMITER //; 多了分号),会导致后续 CREATE PROCEDURE 被截断,报错信息常为 ERROR 1064 (42000) 且定位困难。
错误处理也极易踩坑:
-
DECLARE EXIT HANDLER FOR SQLEXCEPTION不捕获所有错误,例如表不存在(SQLSTATE '42S02')或权限不足('42000')可能跳过 - 建议显式覆盖常见码:
DECLARE EXIT HANDLER FOR SQLSTATE '45000', SQLSTATE '42S02', SQLSTATE '23000' - 调试阶段务必加
GET DIAGNOSTICS CONDITION 1 @sqlstate = RETURNED_SQLSTATE, @errno = MYSQL_ERRNO, @text = MESSAGE_TEXT;并SELECT @sqlstate, @errno, @text;
过程越复杂,越要假设每条 SQL 都可能失败;而 MySQL 存储过程的错误传播机制比应用代码更不透明——这点常被忽略,直到线上出问题才意识到没真正兜住异常。











