应先用SET STATISTICS IO和TIME定位瓶颈:逻辑读高物理读低说明内存计算瓶颈,物理读高需建索引,编译时间接近执行时间提示参数嗅探失效;再查数据分布与统计信息,避免盲目建索引。

看懂 SET STATISTICS IO 和 SET STATISTICS TIME 输出
执行慢的存储过程,第一反应不该是改代码,而是先确认“慢在哪”——是磁盘读太多?CPU 跑太久?还是锁住了?SET STATISTICS IO ON 会告诉你逻辑读、物理读、LOB 读次数;SET STATISTICS TIME ON 显示编译时间和执行时间。这两条必须一起开,缺一不可。
- 逻辑读高但物理读低 → 数据基本在内存里,瓶颈可能在计算或连接逻辑
- 物理读高 → 缺少合适索引,或缓存被频繁挤出
- 编译时间接近执行时间 → 存储过程重编译频繁,检查是否用了本地变量代替参数(导致参数嗅探失效)
- 注意:在 SSMS 中开启后,所有后续语句都会输出统计信息,记得用
OFF关闭
识别参数嗅探失效的典型症状
同一个存储过程,有时快得飞起,有时卡住十几秒,且输入参数只差一点——这大概率是参数嗅探(Parameter Sniffing)翻车了。SQL Server 按首次调用的参数值生成执行计划并复用,而该计划对后续参数未必最优。
- 现象:
EXEC YourProc @id = 1很快,@id = 999999却超时;查看执行计划发现后者走了全表扫描,前者走了索引查找 - 临时解法:加
WITH RECOMPILE(仅限调试,别上线)或在语句末尾加OPTION (RECOMPILE) - 更稳做法:在存储过程中把入参赋给本地变量再查询,例如
DECLARE @local_id INT = @id; SELECT ... WHERE id = @local_id—— 这会让优化器放弃嗅探,转而用平均分布估算 - 注意:
OPTIMIZE FOR UNKNOWN看似优雅,但在 SQL Server 2016+ 中实际效果常不如本地变量可靠
给 WHERE / JOIN 字段建索引前先看数据分布
不是所有 WHERE 条件字段都值得建索引。如果某列只有 3 个取值(如 status TINYINT CHECK (status IN (0,1,2))),建索引几乎没用——SQL Server 会直接选表扫描。
- 查分布:用
SELECT status, COUNT(*) FROM orders GROUP BY status ORDER BY COUNT(*) DESC - 有效索引需满足:高选择性(唯一值 / 总行数 > 5%)、被高频用于过滤或连接、且类型支持 SARG(如避免在字段上用函数:
WHERE YEAR(created) = 2024无法走索引) - 复合索引顺序很重要:等值条件字段放前面(
WHERE tenant_id = ? AND status = ?→(tenant_id, status)),范围条件放后面(AND created > ?) - 别忘了包含列:如果查询只查
SELECT id, name, email,而 WHERE 是tenant_id,那索引可以建为CREATE INDEX IX_tenant_id ON users(tenant_id) INCLUDE (id, name, email),避免回表
避免在存储过程中拼接动态 SQL 时触发全表扫描
动态 SQL(EXEC(@sql) 或 sp_executesql)本身不慢,慢的是它让优化器无法预知执行路径,尤其当 where 条件可选时,容易生成低效计划。
- 错误写法:
WHERE 1=1 AND (@name IS NULL OR name LIKE '%' + @name + '%')——@name IS NULL分支让优化器不敢信任索引,常退化为扫描 - 正确思路:用
IF分支拆开逻辑,或用OPTION (RECOMPILE)让每次执行都重新生成计划 - 更安全的动态构造:用
sp_executesql配合参数化,而非字符串拼接值;例如EXEC sp_executesql N'SELECT * FROM users WHERE name LIKE @pattern', N'@pattern NVARCHAR(100)', @pattern = @name + '%' - 注意:
LIKE '%abc'前导通配符永远无法走索引,这类搜索应考虑全文索引或应用层预处理
真正卡住性能的,往往不是“要不要加索引”,而是“这个条件到底有没有被优化器当成筛选条件”。执行计划里的“实际行数 vs 预估行数”偏差超过一个数量级,就该怀疑统计信息过期或数据倾斜——别急着建索引,先 UPDATE STATISTICS。










