SQL Server查询卡顿?5个索引优化技巧提速3倍
|
2025年9月那个凌晨三点十七分,我盯着SSMS里那条跑了四十二秒的SELECT,手还停在键盘上没敲F5重试——因为上一次重试前,我刚把执行计划拖到右边,放大了那个红色感叹号,旁边弹出的提示是“Missing index (Impact: 98.3)”,而我点开建议语句一看:CREATE NONCLUSTERED INDEX [IX_Orders_CustomerID_Status_CreatedAt] ON [dbo].[Orders] ([CustomerID],[Status]) INCLUDE ([CreatedAt],[TotalAmount],[OrderId])。我原以为加个INCLUDE字段就完事,结果建完索引第二天早上八点,DBA老陈发来截图:同一查询平均回落到十四秒。我说不准是哪个环节漏了,但日志里那个10.172.44.192反复连了七次master库又断开,端口还是1433带TLSv1.2握手失败——这IP我在上周的WAF误报名单里见过三次,不是我们云厂商的网段。 第一个坑:WHERE a=1 AND b=2 ORDER BY c DESC,建了(a,b,c)复合索引,跑得飞快;但某天业务方加了个新需求,改成WHERE a=1 AND d=‘pending’ ORDER BY c DESC,我就在原索引上顺手加了个INCLUDE(d),想着不改结构总归安全。结果执行计划里赫然跳出“Index Seek + Key Lookup”,IO翻了五倍。李工昨天在我工位后站了三分钟没说话,最后只说:“你INCLUDE的是d,可SQL Server查d根本不用SEEK,它得先走a,b索引定位行,再回表查d值来过滤,等于白建。” 我删了INCLUDE(d),补了个(a,d,c)新索引,十三秒变三点二秒。这个我敢打包票,SQL Server不认INCLUDE做筛选条件,哪怕你写在WHERE里它也当空气。
文章配图,仅供参考 服务器上多出来的那个文件叫C:\\Windows\\Temp\\sqllog_20250912_1534.chk,大小是八百九十六字节,创建时间比我们最后一次patch早十八秒。没人动过那台DB服务器的本地账户,但chown显示属主是“NT SERVICE\\SQLServerAGENT”。我让运维小王查了agent job历史,最近七十二小时全是空的。这事跟索引无关,但我记下来了——就像上次溯源到一半,IOC匹配上三个域名,结果第三个跳转进CDN后IP一分为六,线索全断在Cloudflare那层TLS1.3的SNI混淆里。第二个技巧:别信“选择性越高越好”。我亲眼见张总监拿[CreatedDate]建唯一索引,说“时间戳天然高选择性”。结果他那张日均写入五十万的表,所有查询都带WHERE CreatedDate >= ‘2025-09-01’ AND Status IN (‘pending’,‘processing’)——索引扫描还是占七成资源。后来我强行把Status提到第一列,建了(Status, CreatedDate),再把Status换成整数状态码(0/1/2),执行计划立刻切到Seek。但整数状态码这事我说不准,因为测试环境里Status用字符串时同样能走Seek,只有生产库里不行。可能跟统计信息采样率有关,也可能跟凌晨两点自动更新统计信息时那场磁盘抖动有关——那天恰好有台存储节点掉线了三十七秒,没人报障,只是监控曲线有个小凸起。 第三个:DROP INDEX之前先看sys.dm_db_index_usage_stats。我们线上有个IX_User_Email的非聚集索引,半年没被Seek过一次,只有Updates和User_Seeks都是0,但User_Scans是两千三百一十一。我正准备删,小赵从隔壁伸头说:“别动,审计那边还在扫邮箱字段做GDPR比对。” 我愣住,回头去翻他们上周发的接口调用日志,发现所有扫描请求都来自172.16.0.15这个内网地址,路径是/api/v2/compliance/export,UA写着“Python-urllib/3.11”。后来才知道那是法务部外包写的合规脚本,每次启动就全表扫一遍——它根本不在应用监控范围里,也不走API网关,WAF规则里压根没这条路径。这玩意儿就跟那个深夜群里突然@全员的机器人一样,名字叫“audit-scheduler-v2”,头像是一只戴眼镜的仓鼠。 第四个,最糙最有效:把OR干掉。WHERE type = ‘A’ OR type = ‘B’,别傻建(type)单列索引指望SQL Server自动合并,它大概率给你整成Table Scan。要么UNION ALL,要么改type字段为tinyint,再建(type)索引+WHERE type IN (1,2),效果立现。我们有个支付流水表,原来用varchar(10)存‘refund’/‘charge’/‘adjust’,改成tinyint后,索引深度从四层压到两层,B-tree页面分裂次数直接清零。但说实话,这事没测全量数据,就抽样了三百万行,所以我不敢说“永远成立”。不过老陈那台笔记本上的SSMS连测试库都卡,说明他本机统计信息可能早坏了——他连“更新统计信息”按钮右键菜单都点不出来,一直弹“无法连接到数据库引擎”,而他本地SQL Server服务明明开着。 第五个:覆盖索引别贪大。我见过有人把SELECT FROM Orders WHERE CustomerID = ? 的索引,INCLUDE字段拉了十九个列,连JSON字段都不放过。结果插入延迟暴涨,页拆分疯狂,更别说同步到只读副本时log send queue直接堆到四千兆。后来砍掉七个低频字段,保留CustomerId、OrderId、TotalAmount、CreatedAt、Status——够大部分查询覆盖了。但删完那天下午,客服系统告警说“订单详情加载超时”,查出来是前端突然开始查DeliveryNote字段,而它不在新索引里。没人提前知会,就是某个Vue组件悄悄加了v-if=“order.deliveryNote”。这种事防不住,只能定期扫sp_who2里那些blocked session,揪出被Key Lookup卡死的SPID。 现在我把索引优化脚本改成了带注释的:每建一个索引,下面必须跟一行#来源:[需求编号/监控报警ID/SQL Audit ID]。上周五刚上线,运维组就发现有两条INSERT卡在锁等待里,查出来是两个不同微服务同时INSERT同一张配置表——它们都试图UPsert同一条“payment_timeout”记录,而其中一条建了唯一索引,另一条没建,导致锁升级逻辑彻底失序。这事跟索引优化无关,但我打算下周找产品开会时,把那张配置表的建表DDL打印出来贴桌上。先不说技术,就说纸张厚度:四页A4,字比蚂蚁腿细。 明天上午九点,我去拿那台DB服务器的物理内存dump。不是为了索引,是为了搞清楚C:\\Windows\\Temp\\sqllog_20250912_1534.chk到底是谁写的。要是抓不到写入进程,我就手动挂载那个dump,在PAGE_FRAME_NUMBER_ARRAY里搜字符串“chk”。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |


漏洞修复+索引优化:双擎驱动搜索性能跃升
