news 2026/10/11 22:38:41

SQL Server中索引查找退化为索引扫描的原因与排查指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL Server中索引查找退化为索引扫描的原因与排查指南

简介:SQL Server 执行计划中,索引查找(Index Seek)为何会退化为索引扫描(Index Scan)?这份文档以排查思路为主线,面向 SQL Server 开发与运维人员,梳理了导致该问题的 10 类典型场景,包括隐式转换、非 SARG 谓词、谓词选择度过低、统计信息过期或失真、联接条件与索引键不匹配、排序分组需求、索引覆盖不足、高度碎片化、并行计划选择、参数嗅探及资源受限等。文档逐一结合上下文给出测试与结论,并重点演示了隐式转换案例,例如将 NVARCHAR 列与数值常量直接比较时,执行计划会从 Index Seek 变为 Index Scan,文章同时给出两种避免写法并附有用于搜索执行计划中隐式转换的 SQL 脚本。资源为 1 个 PDF 文件,大小约 415KB,内容结构清晰,既包含理论归纳,也提供可操作的优化建议。目前已有 331 人学习下载,适合需要提升查询性能、排查执行计划异常的读者参考。

1. 索引查找变成索引扫描:优化器这笔账算错了,还是本来就该扫?

线上一个订单查询原本毫秒级返回,某天突然变成秒级。抓出执行计划一看,之前的 Index Seek(索引查找)变成了 Index Scan(索引扫描)。索引没坏、数据没丢、重启也解决不了,真正的问题藏在优化器算的那笔成本账里。下面按查询写法、数据统计、缓存计划三层拆开讲 SQL SERVER 里让索引查找退化回索引扫描的原因,配上可直接复现的 SQL 示例,讲清每类怎么识别、怎么修,适合正在做慢查询优化,或者被领导追问“明明加了索引为什么还是扫描”的从业者。

2. 优化器怎么算账:Seek 与 Scan 的成本差先对齐

刚接手这类问题时,我最常看到的动作是:看到执行计划里出现 Index Scan,反手就把责任归给“索引没建好”。其实优化器选 Scan 不是抽签,而是两套成本表达式比大小之后的结果。只有先理解 Seek 和 Scan 在物理上是怎么干活的,后面的排查顺序才有依据。

2.1 B-Tree 两种访问方式,成本构成完全不一样

Index Seek 走的是从根页到中间页再到叶子页的树遍历,每次只读目标键值所在的那一小段叶子页,IO 模型是少量随机读。Index Scan 则是从索引或表的叶子链第一页开始,顺着链接一路读完,IO 模型是顺序读加预读(read ahead),一次能带进一大批区。同样读 1000 个页,随机 IO 的物理耗时远高于顺序 IO,所以优化器对 Seek 和 Scan 的成本估算从来不是按“读页数”一刀切。

两条路径的成本表达式差别很大:Seek 的成本由“树的高度 + 预计命中的页数 + 后续书签查找(如果需要回表)次数”构成;Scan 的成本基本由“索引或表总页数”决定,和最终返回多少行关系不大。当预估返回行数占表总行数的比例升高时,Seek 的成本曲线会陡增,Scan 反而显得便宜。经验上,比例超过 20% 到 30% 时,Scan 经常胜出,这个区间受行宽、页密度、索引深度影响,不是硬阈值。

这也是为什么做这个问题的分析要先立一个观念:Seek 变 Scan 不一定是错事。如果查询确实要读全表的四分之一行,选择性这么差的谓词本来就救不回来,纠结操作符名字没有意义。后面所有排查技巧,本质上都是在回答“优化器算出来的这笔账,依据到底可不可信”。

2.2 用执行计划和 IO 统计,读出“它为什么选了 Scan”

打开实际执行计划,直接看 Index Scan 操作符的“谓词”属性,是第一步。操作符属性里如果只有 Predicate(残差谓词),没有 Seek Predicate,说明这个谓词根本没法定位到 B-Tree 的键范围,只能把相关页全读出来再过滤。如果既有 Seek Predicate 又有 Predicate,说明索引只帮了一部分忙,剩下的条件在叶子上过滤。搞清楚这两者的差别,后面排查方向就不一样了。

配合 IO 统计可以更直观。通常我会先跑一遍 SET STATISTICS IO,把逻辑读记下来作为基线。逻辑读是访问的页数,不受缓存影响,用作前后对比比耗时更稳定。

-- 打开 IO 与时间统计,跑一次查询 SET NOCOUNT ON; SET STATISTICS IO ON; SET STATISTICS TIME ON; SELECT SalesOrderID, OrderDate, CustomerID, TotalDue FROM Sales.SalesOrderHeader WHERE OrderDate >= '2024-01-01T00:00:00' AND OrderDate < '2024-02-01T00:00:00'; GO SET STATISTICS IO OFF; SET STATISTICS TIME OFF;

这段查询的谓词直接落在 OrderDate 列上的范围比较,是 SARGable 的,OrderDate 索引可以被利用。重点看消息选项卡里的 Scan count 和 logical reads:如果这个结果只有几十页,说明走的是 Seek;如果跑出上千页甚至和整表页数差不多,说明要么索引没建对,要么优化器选择了扫描。消息里还会给出 CPU 和耗时,但那个受机器负载影响大,做对比时以 logical reads 为主。

参数说明:SET STATISTICS IO 的输出关键字段是 Scan count(扫描次数)、logical reads(逻辑页访问数)、physical reads(从磁盘读的页数,含预读)。不同写法之间对比时,只对比 logical reads 就能排除缓存干扰。注意把 IO 统计关掉再跑其他查询,不然消息窗口会被刷屏。

再看 Estimated vs Actual:如果 Estimated Rows 和 Actual Rows 差出数量级,问题大概率在统计信息或基数估算;如果两者接近,优化器算的账本来就是扫描更便宜,要改的是查询写法或索引设计,而不是跟优化器较劲。

2.3 书签查找如何把 Seek 的账算崩

还有一个让 Seek 败给 Scan 的常见中间环节是书签查找。非聚集索引只存索引键和定位器,定位器在聚集索引表里是聚集索引键,在堆表里是 RID。走非聚集索引 Seek 拿到定位器后,如果查询要的列不在索引里,得回到聚集索引或堆里取数据,每次回表都是一次随机 IO。

当 Seek 返回的行数一多,回表次数跟着涨,成本很快就超过直接扫一遍。优化器在“非聚集索引 Seek + 书签查找”和“聚集索引 Scan”之间比较,常常选择后者。这类退化的特征很明显:执行计划里同时出现 Index Seek 和 Key Lookup/RID Lookup 两个操作符,而实际走的是 Scan。

应对方向通常三个:把查询要的列补进非聚集索引的 INCLUDE,让 Seek 自给自足;调整索引键顺序让 Seek 本身命中行数变少;或者在确定无法满足时接受 Scan,不要硬塞提示。具体哪些做法会翻车,第 5 章里会展开讲。

3. 查询写法引发的退化:非 SARGable 谓词、隐式转换与 OR 条件

优化器能不能把谓词翻译成索引键上的范围,取决于写法。下面这张表先给结论:谓词里索引列保持原样,是 Seek 的前提。

谓词形态示例优化器的处理
SARGableOrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'可写入 Seek Predicates,沿 B-Tree 定位
非 SARGableYEAR(OrderDate) = 2024无法定位键范围,只能全扫后残差过滤
函数在参数侧OrderDate = DATEADD(year, 1, '2023-01-01')索引列没被碰,Seek 可保留
函数在列侧DATEADD(year, 1, OrderDate) = '2024-01-01'索引列被包进表达式,Seek 失效

3.1 函数套在索引列上,Seek 条件直接失效

所谓 SARGable,是 Search Argument Able 的缩写,意思是“这个谓词能被索引键直接利用”。索引列原样和常量/参数做比较,优化器才能算出键的起点和终点。只要索引列被包进函数或表达式,比如 YEAR(OrderDate)、DATEADD(day, -1, OrderDate)、列 + 1、LEFT(列, 3),优化器就失去了定位起点,只能把所有相关页读出来,逐行做一次残差过滤。

这里有个容易误解的地方:执行计划里 Scan 操作符的 Predicate 属性和原 SQL 条件长得一模一样,看起来好像没毛病,所以很多新手以为是索引丢了。其实正是由于索引列上套了函数,这个谓词没法搬到 Seek Predicate 里,才以残差身份挂在 Scan 后面。识别的关键就是看谓词在计划里出现在哪个位置。

-- 翻车写法:索引列上套 YEAR 函数 SELECT SalesOrderID, TotalDue FROM Sales.SalesOrderHeader WHERE YEAR(OrderDate) = 2024; -- 修正写法:范围比较,索引列保持原样 SELECT SalesOrderID, TotalDue FROM Sales.SalesOrderHeader WHERE OrderDate >= '2024-01-01T00:00:00' AND OrderDate < '2025-01-01T00:00:00';

第一段的 YEAR 函数让优化器无法在 OrderDate 上构建 Seek 谓词,只能把 2024 年涉及的叶子页全扫出来再过滤,逻辑读通常是第二段的几十倍。第二段改成半开区间后,优化器能在 B-Tree 上算出第一个大于等于 2024-01-01 的键位置,直接 Seek 进入,沿叶子链顺序读到 2025 年前结束,IO 量大幅下降。

参数说明:日期范围统一写成“大于等于起点且小于下一个起点”的半开区间,比 BETWEEN 更稳,能避开 datetime 与 datetime2 在边界精度上的换算。如果查询条件来自应用程序,先把参数在传入前转成目标类型,不要写 CONVERT(列, ...) 这类把转换放在列上的写法。

3.2 隐式转换:VARCHAR 列遇 NVARCHAR,列上多了一次 CONVERT

SQL Server 的数据类型之间有隐式转换优先级规则,低优先级向高优先级转换。规则不复杂:VARCHAR 低于 NVARCHAR,INT 低于 BIGINT,日期类型里 DATETIME 低于 DATETIME2。当比较发生在列和常量/参数之间,SQL Server 会选择把低优先级的一方转成高优先级,这个转换如果发生在索引列上,Seek 就废了。

最常见的一幕是应用层用 NVARCHAR 参数去查 VARCHAR 列。参数是 NVARCHAR,优先级高,SQL Server 就把列隐式转成 NVARCHAR 再比较。索引键上被包了一层 CONVERT_IMPLICIT,本来可以精确命中的等值查询,退化成了把整个索引叶子页扫一遍再逐个比较。而反过来,索引列是 INT,参数传的是字符串 '12345',字符串优先级低,转换发生在参数侧,索引列没被碰,Seek 能保住。判断在哪一侧,打开实际执行计划找 CONVERT_IMPLICIT 出现在哪个 Input 上即可。

-- 场景:SalesOrderNumber 列是 VARCHAR(20),参数是 NVARCHAR DECLARE @OrderNumber NVARCHAR(20) = N'SO50001'; SELECT SalesOrderID, SalesOrderNumber FROM Sales.SalesOrderHeader WHERE SalesOrderNumber = @OrderNumber; -- 修正:参数类型与列一致 DECLARE @OrderNumber2 VARCHAR(20) = 'SO50001'; SELECT SalesOrderID, SalesOrderNumber FROM Sales.SalesOrderHeader WHERE SalesOrderNumber = @OrderNumber2;

第一段里 N'SO50001' 是 NVARCHAR,优先级高于列,SQL Server 选择把列转成 NVARCHAR。执行计划中 SalesOrderNumber 输入节点上出现 CONVERT_IMPLICIT,索引查找退化成索引扫描。第二段把参数改成 VARCHAR,类型匹配,转换消失,计划回到 Index Seek。实际的逻辑读差距在索引页较多时非常明显。

参数说明:如果参数来自 ORM 或接口层,经常统一走 NVARCHAR,这时候要看具体的列类型来决策。表列是 VARCHAR 时,优先把存储过程的入参也定义为 VARCHAR;如果应用层无法改,有限范围内可以考虑把列改成 NVARCHAR,但要提前确认磁盘空间和现有 VARCHAR 参数,免得改完列,换 VARCHAR 参数反过来去转换 NVARCHAR 列。JOIN 两表字段类型不一致时同理,先把两边的实际数据类型查出来再定方案。

3.3 OR 条件的两副面孔:能展开成 Seek,也能摊平成 Scan

OR 条件很容易被翻车。如果 OR 的几个分支都精确匹配同一列的索引值,优化器通常能把它拆成多个等值 Seek,效果类似 IN 列表。比如 WHERE Status = 3 OR Status = 5 在 Status 有索引时可以走 Seek。但一旦 OR 分支里混入了非 SARGable 条件,或者两个分支落在不同索引列上,整个谓词常常没法成为连续的 Seek 范围,优化器转向扫描。

-- OR 条件落在同一索引列上的典型场景,优化器可拆成 Seek SELECT SalesOrderID FROM Sales.SalesOrderHeader WHERE Status = 3 OR Status = 5; -- OR 一边能 Seek,一边套函数,整体退化 SELECT SalesOrderID FROM Sales.SalesOrderHeader WHERE SalesPersonID = 285 OR YEAR(OrderDate) = 2024;

第一段两个等值条件在同一列上,优化器可以把它处理成多个 Seek 谓词。第二段右边是非 SARGable,整个谓词无法准确映射到索引键范围,即便 SalesPersonID 上有索引,优化器也可能放弃它。实际计划里也可能出现“SalesPersonID Seek + 回表再过滤右边”的折中方案,具体看选择性;重点是排查时先看最弱的那个 OR 分支,它决定了整个谓词能不能被索引接住。

参数说明:遇到这种结构,常见做法是把 OR 分支拆成两条查询用 UNION ALL 合并,让每条查询独立走索引。不要用 UNION,去重排序的开销在这种场景里是白付的。如果 OR 分支来自同一列的不同值,优先改写成 IN 列表,计划更简洁,也方便基数估算。

4. 数据与环境的退化:统计信息失真、参数嗅探与基数估算翻车

写法没问题,索引也没错,那问题就出在优化器拿到的“情报”上:统计信息不准、缓存计划被污染、基数估算模型判断失误。这一层最像玄学,但每一条都能用脚本验证。

4.1 统计信息蒙住优化器的眼睛:估算行数决定计划的底座

优化器在决定走 Seek 还是 Scan 之前,先要回答“这个谓词预计返回多少行”。这个答案来自统计信息的直方图:SQL Server 把索引列的值域按步长切分,记录每段的行数。直方图最多保留约 200 个步长,遇到数据分布极不均匀的列,尖峰很容易被合并进某一段,估算行数会和实际差出数量级。

还有采样率问题。自动更新统计信息默认按抽样比例采样,大表抽样率低时,直方图与真实分布偏差更大。另外自动更新有触发阈值,旧版本是“变更行数超过表行数的 20% 左右”才触发,SQL Server 2016 之后对不同大小的表用了动态阈值,但对超大表仍然存在滞后窗口。批量灌数据之后没达到阈值、或者索引刚重建完没来得及更新,都是典型的退化窗口。

-- 查看统计信息的具体分布 DBCC SHOW_STATISTICS('Sales.SalesOrderHeader', IX_SalesOrderHeader_OrderDate); -- 手动刷新(数据倾斜列建议 FULLSCAN) UPDATE STATISTICS Sales.SalesOrderHeader IX_SalesOrderHeader_OrderDate WITH FULLSCAN;

第一句返回三个结果集:统计信息头、密度、直方图。重点看直方图里的 RANGE_HI_KEY、EQ_ROWS、AVG_RANGE_ROWS,如果某段区间实际新增了大量数据,直方图里没有对应的尖峰,说明统计过期了。第二句用 FULLSCAN 全表重建直方图,分布最真实,代价也高,适合放在维护窗口对问题索引单独执行。

参数说明:sp_updatestats 会把库内所有表的统计信息刷一遍,动作粗且耗时,不建议当成首选。先定位到具体索引,查看直方图确认失真程度,再决定是否用 FULLSCAN。对大表可以先按默认采样刷新一次看估算有没有回正,回正就不必 FULLSCAN 了。

4.2 参数嗅探:一个倒霉的第一次,让所有精查询共用扫描计划

参数化查询第一次编译时,优化器会把传入的参数实际值代入统计信息做估算。这个第一次如果碰上匹配大量行的“脏参数”,生成的计划就是 Scan。这计划被缓存下来,之后所有参数值执行时都复用这份 Scan 计划,哪怕某个参数值只匹配个位数行,索引查找也永远不再出现。

识别参数嗅探有一个很朴素的方法:同一个存储过程或批处理,在图形计划里看是 Scan,单独加一句 OPTION (RECOMPILE) 重跑,计划变成 Seek,而且耗时明显下降。这种“同一个写法、换个执行环境计划就变了”的差异,基本就是缓存计划在作祟。

-- 1) 定位目标语句的 plan_handle SELECT qs.plan_handle, st.text, qs.execution_count, qs.last_execution_time FROM sys.dm_exec_query_stats AS qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st WHERE st.text LIKE '%SalesOrderHeader%' AND st.text LIKE '%CustomerID =%'; -- 2) 清除该语句的缓存计划,让它重新编译 DBCC FREEPROCCACHE(0x06000600...); -- 3) 语句级重新编译,避免复用旧计划 SELECT SalesOrderID, OrderDate FROM Sales.SalesOrderHeader WHERE CustomerID = @CustomerID OPTION (RECOMPILE);

第一步在 dm_exec_query_stats 里找到目标语句的 plan_handle,第二步只清掉这条计划,不影响其他缓存。第三步的 RECOMPILE 让这条语句每次执行都重新编译,按当前参数实际值估计划,代价是每次多花一点编译时间。

参数说明:OPTION(RECOMPILE) 适合执行频率低、参数漂移严重的语句。一秒执行几十次的语句不建议加,编译开销会放大。中低频场景用 RECOMPILE 常用常新;SQL Server 2022 的 Parameter Sensitive Plan Optimization 会自动为不同参数值区间缓存多份执行计划,减少手动干预,升级到 2022 后这类场景可以少写提示。

4.3 基数估算翻车:多谓词相关性让选择性被算错

SQL Server 2014 开始默认使用新的基数估算(CE)模型。新模型对多重谓词的处理和旧模型不一样,常见的问题是低估或高估叠加过滤后的行数。比如查询里同时过滤 CustomerID 和 OrderRegion,这两个列如果业务上本来就强相关,新 CE 按独立性假设把两个选择性相乘,估算结果远低于实际行数,优化器可能生成 Seek + 大量回表,也可能反过来因为高估而选 Scan。

这种问题在计划里的特征很明确:Estimated Rows 和 Actual Rows 相差数十倍,而且同一条语句换个参数值,计划在 Seek 和 Scan 之间横跳。排查时可以先把谓词拆开,逐个条件单独执行看估算是否准确,再用多列统计信息补相关性。多列统计信息能缓解但管不了所有场景。

-- 这条语句强制使用旧版基数估算,做 A/B 对比 SELECT SalesOrderID, OrderDate FROM Sales.SalesOrderHeader WHERE CustomerID = @CustomerID AND OrderRegion = @OrderRegion OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION') 让当前语句回到 2014 前的 CE 模型,适合判断“新 CE 对这条查询的估算是否翻车”。如果换旧 CE 后计划变回 Seek 且 Actual Rows 恢复正常,说明是 CE 模型对数据相关性的判断问题,而不是索引和写法的问题。

参数说明:这种开关只用于对照验证,不解决根本。根本手段还是让优化器拿到准确的统计信息、让谓词 SARGable、让选择性真实的索引存在。Trace Flag 9481 和 4199 也能全局切换 CE 或启用修复项,但影响面大,不到万不得已不建议在生产库全局开,先在干净会话里验证。

5. 避坑清单:索引查找退化里踩过的 5 个坑与排查顺序

下面这 5 个场景都是实际维护中容易被误导的案例,按“现象 → 原因 → 解决”的顺序写,遇到类似情况可以对照着排查。

5.1 统计信息刷新了,计划还是扫描

现象:线上查询从 Seek 变 Scan,按常规先执行了 sp_updatestats,再看计划,依旧扫描。

原因:sp_updatestats 是大规模刷新,对超大表可能采用较低采样率,直方图没有捕捉到关键的倾斜数据分布;甚至可能因为时间窗口或跳过条件,出问题的那个索引统计没被更新。另一个可能是统计更新了,但直方图步长合并后依然刻画不了尖峰。

解决:针对具体索引执行 UPDATE STATISTICS ... WITH FULLSCAN,然后重新查看 DBCC SHOW_STATISTICS,确认直方图里对应键值的 EQ_ROWS 发生了明显变化。如果 FULLSCAN 后还是 Scan,再看 XML 执行计划里的 StatisticsInfo 节点,确认引用的统计信息是不是刚刷的那一份,避免计划缓存里还是旧版本的估算。

5.2 加了 INCLUDE 列之后,Scan 变 Seek 但查询更慢了

现象:给索引补上 SELECT 需要的 INCLUDE 列后,执行计划从 Index Scan 变成了 Index Seek,但查询耗时变大,SET STATISTICS IO 里 logical reads 比之前还高。

原因:Scan 有预读和顺序 IO 兜底,Seek 是按键值逐条随机读。当 Seek 返回的行数分散在大量数据页上时,即使占比不高,随机 IO 的开销也会超过一次完整顺序扫描。执行计划看起来“健康”了,物理代价反而更大。

解决:不要只看操作符,要看 logical reads 和执行总耗时。遇到这种情况,优先调整索引键顺序,让 Seek 谓词对应的是索引最前导列,缩小 Seek 返回行数;如果查询条件自身选择性就不足 5% 到 10%,Scan 可能本来就是正确答案,别硬扭。

5.3 FORCESEEK 验证时很香,上线更慢

现象:怀疑优化器算错账,加了 WITH (FORCESEEK) 强制走 Seek,测试环境执行很快;发布到生产后响应时间更差。

原因:FORCESEEK 直接绕过了成本比较,强制生成 Seek 计划。匹配行数越多,后续回表的次数越多,随机 IO 被放大。测试环境数据量小,这条增长曲线还没起来,看不出问题。

解决:FORCESEEK 当诊断工具用,不当修复手段。看到 Seek 计划能跑通、能确认问题不在写法之后,马上去掉提示,回到索引层面解决。给索引补 INCLUDE、调整前导列、改写谓词让 Seek 本身变便宜,才是正路。

5.4 JOIN 键两边都是 INT,计划里却出现了 CONVERT_IMPLICIT

现象:两个表关联字段定义都是 INT,执行计划里却看到 CONVERT_IMPLICIT 转换,关联列上的索引没有用上。

原因:一边是 INT,另一边可能是 SMALLINT 或 TINYINT。INT 优先级高,低优先级的列会被提升成 INT,如果被提升的列恰好在索引键上,索引就被包了一层转换,Seek 失效。两边都叫 XXID,看着像同类型,实际长度和类型族不同,是最常见的翻车点。

解决:先用 sys.columns 查询两边的精确类型。应用层不好改时,给非索引侧做显式 CAST,保住索引侧不被转换。比如 WHERE 大表.索引列 = CAST(小表.关联列 AS INT),把转换挪到小表一方,索引列保持原样。

5.5 IS NULL 谓词让优化器“没法估”

现象:查询里有 WHERE DeletedFlag IS NULL,这个条件选择性其实很好,却总是走扫描,给列建了普通索引也没改善。

原因:统计信息的直方图里没有 NULL 对应的条目,优化器对 NULL 的选择性几乎没有参考数据,只能按默认猜测处理,Seek 的收益算不出来,自然选了更保守的 Scan。

解决:业务上给 NULL 一个明确的默认语义,比如删除状态列一律填 0 表示未删除,查询改成 WHERE DeletedFlag = 0,普通索引就能接住。另一种做法是建过滤索引:CREATE INDEX IX_SalesOrderHeader_DeletedFlag ON Sales.SalesOrderHeader (DeletedFlag) WHERE DeletedFlag IS NULL; 过滤索引自带独立的统计信息,IS NULL 这条路就有据可依了。注意过滤索引要覆盖查询里用到的其他列,避免回表太多。

6. 最后一里路:用 A/B 验证给结论上保险

排查到这一步,手上通常有候选方案:改写谓词、更新统计信息、调索引键顺序、补 INCLUDE。但任何改动上线前,都要先做一次可对比的验证。我的标准动作是先记录基线,再改动,再对比,最后才决定上不上线。

-- 基线:原始查询 SET STATISTICS IO, TIME ON; SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE YEAR(OrderDate) = 2024; SET STATISTICS IO, TIME OFF; GO -- 改动后:改写谓词,同时标记计划 SET STATISTICS IO, TIME ON; SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE OrderDate >= '2024-01-01T00:00:00' AND OrderDate < '2025-01-01T00:00:00'; SET STATISTICS IO, TIME OFF; GO

对比时先看 logical reads,再看耗时。逻辑读是页访问次数,不受缓存影响;耗时受机器负载影响,只作辅助。两段查询之间不要清缓存,清缓存会引入预读波动,干扰对比。如果两张计划一个 Seek 一个 Scan,但 logical reads 接近,那改写收益不大;只有 logical reads 明显下降,才算真正优化成功。有条件的话,在测试实例上把两条 Select 放进同一批处理里各跑三次取中位数,能避开抖动。

我自己早期调索引,只看执行计划从 Scan 变成 Seek 就在记录里写“优化完成”,后来对比 logical reads 才发现从 200 涨到 900,脸被打肿。之后所有改动都先留基线,再谈上线。写 SQL 调优,操作符只是表象,IO 数字才是体检报告。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 22:37:47

手写数字识别毕设工程化:从MNIST到真实场景的完整落地实践

简介&#xff1a;本资源是一套面向本科毕业设计、课程设计及期末大作业的高分Python手写数字识别完整项目&#xff0c;适用于人工智能入门学习者与计算机相关专业学生&#xff0c;解决从模型构建、训练到部署演示的全流程实践需求。压缩包共28个文件&#xff0c;约29.22MB&…

作者头像 李华
网站建设 2026/10/11 22:36:05

P1348公交网建设:最小生成树Prim与Kruskal算法深度解析

1. 题目拆解&#xff1a;公交网建设到底在考什么P1348这道题&#xff0c;乍一看是城市公交网建设&#xff0c;好像是个规划问题&#xff0c;但剥开外壳就是一道非常典型的**最小生成树&#xff08;MST&#xff09;**问题。这类题目在信息学奥赛里属于"模板题中的变式"…

作者头像 李华
网站建设 2026/10/11 22:34:09

Java 接 YOLO ONNX:跨语言视频目标检测落地实践

简介&#xff1a;本资源面向需要在 Java 项目中落地视频目标检测的开发者&#xff0c;提供一套 Java 调用 Python YOLO ONNX 模型的完整方案&#xff0c;支持 YOLOv5、YOLOv7、YOLOv8 等主流模型&#xff0c;并覆盖 RTSP/RTMP 视频流处理场景。整体架构由 Java 端负责视频流获取…

作者头像 李华
网站建设 2026/10/11 22:32:56

6G的应用场景在哪

如果要用一句话来概括&#xff0c;6G的应用场景不再只是“把信息传得更快”&#xff0c;而是让网络本身变成一个能感知、会思考、可协同的“超级智能体”。它服务的对象将从“人”和“物”的简单连接&#xff0c;扩展到“人、机、物、境”的深度协同。根据国际电信联盟&#xf…

作者头像 李华