Prepared Statement二次执行变慢,主因是EXECUTE时优化器未复用执行计划:参数隐式类型转换、统计信息更新或驱动假复用等导致重解析与重优化。

Prepared Statement 二次执行本不该有明显性能损耗——如果真慢了,问题不在“复用”本身,而在复用时的上下文没对上。最常见的情况是:你以为在复用,其实每次都在重新解析、重新生成执行计划,甚至触发隐式类型转换或索引失效。
为什么 EXECUTE 第二次比第一次还慢?
这通常不是 MySQL 的 bug,而是参数绑定或环境变化导致优化器无法命中缓存的执行计划。PREPARE 阶段只缓存语法树和部分元数据,但 EXECUTE 时仍要走优化器流程;若参数值引发隐式类型转换(比如 INT 字段传入字符串参数)、或表统计信息已更新、或 SQL_NO_CACHE 被误设,就可能跳过计划缓存,退化为全量重优化。
- 检查是否发生隐式转换:用
EXPLAIN FORMAT=TRADITIONAL对比两次EXECUTE的输出,看type和key是否一致,Extra里是否出现Using where; Using index→Using where这类降级 - 确认参数类型与字段类型严格匹配:比如
id INT就别传@a := '123'(字符串),而要用@a := 123 - 避免在 PREPARE 前执行
ANALYZE TABLE或大事务提交后立刻EXECUTE,统计信息刷新可能让优化器弃用旧计划
如何验证执行计划是否真的被复用?
MySQL 本身不直接暴露“该 EXECUTE 是否用了缓存计划”,但可通过组合手段交叉验证。关键不是看有没有 PREPARE,而是看优化阶段有没有被跳过。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 开启 optimizer_trace:
SET optimizer_trace="enabled=on,one_line=off";,然后执行EXECUTE,再查SELECT * FROM information_schema.OPTIMIZER_TRACE;对比两次 trace 中"steps"数量——若第二次仍有完整join_optimization+join_execution,说明没复用 - 观察
Handler_read_*状态变量变化:SHOW SESSION STATUS LIKE 'Handler%read%';执行前后做差;若第二次Handler_read_next暴增,大概率走了全表扫描,计划未复用 - 注意:MySQL 5.7+ 默认不缓存 Prepared Statement 的执行计划;8.0.22+ 才在特定条件下启用(如无子查询、无用户变量);GaussDB(for MySQL) 等增强版才默认开启 plan cache
客户端驱动层的“假复用”陷阱
Java 应用里看到 PreparedStatement.execute() 很快,不代表数据库端真复用了。JDBC 驱动可能做了自己的缓存(比如 mysql-connector-java 的 cachePrepStmts=true),但它只缓存 Statement 对象,不控制服务端行为。服务端仍可能因连接切换、session 变量变更(如 sql_mode)、或 max_prepared_stmt_count 触顶而丢弃句柄。
- 检查服务端当前 prepared statement 数量:
SELECT COUNT(*) FROM performance_schema.prepared_statements_instances; - 确认连接未被连接池回收重建:比如 HikariCP 的
maxLifetime小于语句生命周期,会导致每次execute()实际连的是新 session,PREPARE 重来 - 禁用驱动侧干扰:临时把 JDBC URL 加上
&cachePrepStmts=false&useServerPrepStmts=true,强制走服务端预处理逻辑,排除客户端缓存干扰
真正影响二次执行性能的,从来不是“有没有 PREPARE”,而是“EXECUTE 时优化器有没有偷懒”。很多团队花时间调优 SQL 文本,却忽略 @var 的类型、连接生命周期、甚至 autocommit 开关对计划缓存的影响——这些细节不盯住,再标准的 PREPARE/EXECUTE 流程也会退化成普通查询。










