核心原因是参数嗅探:SQL Server首次执行时依据实际参数值生成并缓存执行计划,后续调用无论参数如何变化均强制复用该计划,导致数据分布差异大时性能骤降。

核心原因不是“缓存没起作用”,而是执行计划被首次参数带偏,或内部对象未清理导致后续执行路径劣化。
SQL Server 中参数嗅探让首次参数决定后续命运
SQL Server 存储过程第一次执行时,会“嗅探”@status、@id 这类输入参数的实际值,并据此生成执行计划。这个计划会被缓存复用——哪怕第二次传的是完全不同的值。
常见错误现象:
- 首次调用
EXEC proc @status = 'Archived'(只占 0.1% 行),生成了嵌套循环 + 索引查找计划 - 后续调用
EXEC proc @status = 'Active'(占 95% 行),仍硬套同一计划,逻辑读暴涨、CPU 拉满 -
sys.dm_exec_query_stats中execution_count上升,但total_logical_reads波动剧烈
实操建议:
- 用
DECLARE @local_status VARCHAR(20) = @status;再查,切断参数直连,让优化器改用统计信息平均估算 - 对典型值明确的场景,加
OPTION (OPTIMIZE FOR (@status = 'Active')),比OPTION (RECOMPILE)更轻量 - 避免写
OPTIMIZE FOR (@status = NULL):NULL 在直方图里是独立桶,容易触发全表扫描
MySQL 存储过程中 PREPARE 未释放引发元数据争用
在 MySQL 存储过程里用 SET @sql = CONCAT(...); PREPARE stmt FROM @sql; 是常见写法,但若漏掉 DEALLOCATE PREPARE stmt,每次调用都会累积一个预处理对象。
常见错误现象:
- 首次执行快(对象少、锁竞争低)
- 多次调用后
SHOW STATUS LIKE 'Com_prepare_sql'明显高于Com_dealloc_sql - 后续执行卡在
Waiting for table metadata lock或解析耗时上升
实操建议:
- 每个
PREPARE stmt后必须紧跟EXECUTE stmt和DEALLOCATE PREPARE stmt - 不要在循环内反复
PREPARE同一语句;提取为一次准备、多次执行 - 检查临时表是否残留:
CREATE TEMPORARY TABLE后必须配对DROP TEMPORARY TABLE,否则可能干扰后续初始化
字符集/排序规则不一致破坏索引选择
当存储过程参数声明为 VARCHAR(64) CHARSET utf8mb4,而目标字段是 utf8mb4_0900_as_cs,WHERE 条件中又没强制对齐,MySQL 可能放弃走索引。
为什么首次快、后续慢?因为首次靠统计信息“蒙对”了执行路径,后续因数据分布变化或缓存了错误计划暴露问题。
实操建议:
- 统一数据库、表、列、连接、存储过程参数的
CHARACTER SET和COLLATION - 关键条件中显式指定:
WHERE name = @param COLLATE utf8mb4_0900_as_cs - 用
EXPLAIN FORMAT=TRADITIONAL对比首次与后续执行的key和rows字段是否一致
ASP.NET 调用时慢,SSMS 执行却快
这不是网络或代码问题,而是连接上下文差异:SSMS 每次新建连接默认清空计划缓存,而 ASP.NET 连接池长期复用同一连接,持续沿用首次生成的低效计划。
更新存储过程(ALTER PROCEDURE)之所以“变快”,是因为它强制清除了该过程所有缓存计划,下次调用重新编译。
实操建议:
- 上线前检查
SET ARITHABORT是否一致:两个同名过程,一个设了ON、一个没设,SQL Server 就视为不同上下文,各自编译 - 高频 OLTP 场景慎用
OPTION (RECOMPILE):每次执行都重优化,CPU 压力翻倍 - 确认统计信息是否过期:
DBCC SHOW_STATISTICS('Orders', 'IX_OrderDate')查看Rows Sampled和Modification Counter
真正麻烦的是多种因素叠加:比如一个带 PREPARE 的 MySQL 存储过程,里面又建了临时表,还传入了未对齐字符集的字符串参数——单点检查都正常,合起来就稳稳变慢。排查时得从 plan_generation_num、Com_prepare_sql、EXPLAIN 这些具体指标出发,而不是猜“是不是缓存坏了”。











