SQL Server存储过程性能异常:.NET服务调用时存在性校验耗时过高
我们在使用Microsoft Enterprise Library 3.0开发的.NET Windows服务中调用SQL Server存储过程时遇到了严重的性能问题。该存储过程的核心逻辑是:检查指定组合字段的记录是否存在,若不存在则插入表中,否则直接返回。
表结构与索引情况
涉及的表结构如下:
create table AlarmLog ( Id bigint primary key clustered, MessageId int, MessageTime datetime, ControllerId int, InterfaceHardwareId int, IDType int, MapId int, RelatedEmployeeId int, RelatedCardId int );
其中Id是主键且带有聚集索引。
根据业务规则,插入时需保证MessageId, MessageTime, ControllerId, InterfaceHardwareId, IDType, MapId这六个字段的组合唯一,因此存储过程中使用if exists语句进行存在性检查,但这个步骤耗时极长。
测试与生产环境的差异
我们尝试为上述六个字段的组合添加非聚集索引后,出现了截然不同的表现:
- 测试环境(SQL Server 2008 Standard,约300万条记录):插入速度可达400+行/分钟,添加索引后性能提升明显;
- 生产环境(SQL Server 2012 Standard SP3,约30万+条记录):无论是否添加该非聚集索引,性能都没有明显改善——保留存在性校验时仅能插入10-20行/分钟,移除校验后插入速度立刻恢复到300-400+行/分钟。
更奇怪的是,在SQL Server Management Studio中手动执行该存储过程插入单条记录时完全没有性能问题,实际执行计划显示没有表扫描,存储过程本身也没有耗时。
已尝试的调优操作
我们已经试过以下常规性能优化手段,但均未起效:
- 重建/重组索引
- 更新统计信息
- 重编译存储过程
- 解决参数嗅探问题(比如使用
OPTION (RECOMPILE)或者局部变量传递参数)
现在我们陷入了困境,希望能得到针对性的建议和指导。
给你的几个针对性建议
1. 换用MERGE语句替代IF EXISTS+INSERT的组合
原来的IF EXISTS会先做一次索引查找,然后INSERT的时候因为要维护索引/唯一约束,又会再做一次检查,两次操作的开销叠加起来就很可观。改用MERGE可以把这两步合并成一个原子操作,让SQL Server优化器生成更高效的执行计划,减少重复的IO开销。示例代码如下:
MERGE INTO AlarmLog AS target USING (VALUES (@MessageId, @MessageTime, @ControllerId, @InterfaceHardwareId, @IDType, @MapId, @RelatedEmployeeId, @RelatedCardId)) AS source (MessageId, MessageTime, ControllerId, InterfaceHardwareId, IDType, MapId, RelatedEmployeeId, RelatedCardId) ON target.MessageId = source.MessageId AND target.MessageTime = source.MessageTime AND target.ControllerId = source.ControllerId AND target.InterfaceHardwareId = source.InterfaceHardwareId AND target.IDType = source.IDType AND target.MapId = source.MapId WHEN NOT MATCHED THEN INSERT (MessageId, MessageTime, ControllerId, InterfaceHardwareId, IDType, MapId, RelatedEmployeeId, RelatedCardId) VALUES (source.MessageId, source.MessageTime, source.ControllerId, source.InterfaceHardwareId, source.IDType, source.MapId, source.RelatedEmployeeId, source.RelatedCardId);
2. 排查生产环境的锁等待问题
你提到SSMS里单条执行没问题,但服务调用就慢,很大可能是服务是并发调用存储过程,导致生产环境出现了锁竞争。可以跑下面的查询看看当前的锁等待情况:
SELECT session_id, wait_type, wait_time_ms, blocking_session_id, resource_description FROM sys.dm_os_wait_stats WHERE wait_type LIKE '%LCK%' ORDER BY wait_time_ms DESC;
如果看到大量的LCK_M_S或者LCK_M_X等待,那就是锁竞争的问题了。这种情况下,开启数据库的快照隔离可以有效缓解:
ALTER DATABASE YourDatabase SET READ_COMMITTED_SNAPSHOT ON;
开启后,读取操作不会阻塞写入,写入也不会阻塞读取,能大幅减少锁等待的时间。
3. 检查参数类型是否完全匹配
一定要确认.NET服务传递的参数类型和SQL Server表中字段的类型完全一致,比如MessageTime在.NET里是DateTime还是DateTimeOffset?如果类型不匹配,SQL Server会做隐式转换,这会导致索引无法被有效利用——哪怕执行计划显示用了索引,实际也是转换后再查找,性能会暴跌。
4. 捕获生产环境的实际执行计划
SSMS里的执行计划和服务调用时的可能不一样,比如你说解决了参数嗅探,但说不定服务调用的参数分布和你手动测试的完全不同,导致优化器选了差的计划。可以用这两种方式捕获实际执行计划:
- 用SQL Server Profiler跟踪
Showplan XML事件; - 启用Query Store(SQL Server 2012支持),查看存储过程的实际执行计划和性能数据。
通过实际执行计划就能清楚看到,服务调用时是不是真的用上了非聚集索引,还是出现了索引扫描之类的低效操作。
5. 检查生产环境的磁盘IO性能
有时候生产环境的磁盘IO是隐形瓶颈,比如磁盘IOPS不够、延迟太高。可以跑下面的查询看看磁盘的IO统计:
SELECT physical_name, avg_read_stall_ms, avg_write_stall_ms, io_stall_read_ms, io_stall_write_ms FROM sys.dm_io_virtual_file_stats(NULL, NULL) JOIN sys.master_files ON sys.dm_io_virtual_file_stats.database_id = sys.master_files.database_id AND sys.dm_io_virtual_file_stats.file_id = sys.master_files.file_id;
如果avg_read_stall_ms或者avg_write_stall_ms超过20ms,那磁盘IO肯定有问题,得去排查存储系统的配置,比如RAID是不是没做好,磁盘是不是老化了,或者存储阵列负载太高。
内容的提问来源于stack exchange,提问作者Ujjal Sharma

