☰
SQL Server索引查找退化成索引扫描:五大根因排查与修复实践
2026/10/3 5:17:33 网站建设 项目流程

简介:SQL Server 中,查询执行计划从索引查找(Index Seek)退化为索引扫描(Index Scan),通常是性能下降的重要信号。这份 PDF 资料面向 SQL Server DBA、开发人员与性能调优者,系统汇总了导致该问题的十类典型原因:隐式转换、非 SARG 谓词、统计信息不准确、连接操作、排序与分组、索引覆盖不足、索引碎片、并行计划、资源限制及参数嗅探,并结合 AdventureWorks 示例数据库逐一说明复现场景、执行计划特征与优化思路。资料为单个 PDF 文件,共 1 个文件,约 415 KB;内容包含问题复现的 T-SQL 语句、查看执行计划的方法、避免隐式转换的数据类型匹配建议,以及从缓存计划中搜索隐式转换的排查脚本,可帮助读者快速定位执行计划劣化的原因,并在实际项目中完成索引设计、查询改写与统计信息维护。目前已有 331 人学习下载,适合作为 SQL Server 索引调优与执行计划分析的便携参考。

1. 索引查找为什么会退化成索引扫描:先给结论再讲场景

SQL SERVER 里最让人头疼的慢查询,往往不是没有索引,而是有索引却走了索引扫描。Index Seek 变成 Index Scan,在开发环境那点数据量上根本看不出来,数据一上生产,一条本该毫秒级返回的查询直接变成几秒甚至几十秒。这个退化不是优化器随机抽风,背后基本是五类原因:非 SARG 谓词让索引失效、统计信息过期导致基数估计偏离、参数嗅探把坏计划缓存成默认值、索引键列顺序设计失误,以及隐式转换和 OR 条件这类查询写法问题。这篇笔记把每一类的判断依据、排查语句和修复路径拆开讲,适合正在用执行计划追慢查询的开发者和维护生产库的 DBA。

2. 从执行计划识别 Seek 退化:操作符、估计行数与逻辑读对比

2.1 Index Seek 和 Index Scan 的本质差别:B树遍历与全量读取

先明确概念。索引查找(Index Seek)走的是 B 树路径:从根页逐层下探到叶节点,只读取满足边界条件的那些键值。索引扫描(Index Scan)则是把叶节点的全部条目从头到尾过一遍。两者的代价模型完全不同:Seek 的 IO 与命中行数和树高相关,Scan 的 IO 与索引整体大小相关。

一棵三层 B 树,根页加中间页加叶页,Seek 一次定位最多读三四个页;Scan 要把所有叶页全读一遍。100 万行、每页 100 行的索引,Scan 要读一万个页,Seek 只读少数几个页再加命中数据的页。这个数量级差异就是为什么优化器要精打细算——每错一次,就是几个数量级的 IO 差距。

但这里有个反直觉结论:扫描不一定比查找差。当查询返回的行数占全表比例超过某个经验区间(一般认为是 5%~10%),优化器会主动放弃 Seek 改用 Scan。因为 Seek 加逐行回表是随机 IO,Scan 是顺序 IO,机械硬盘时代随机 IO 比顺序 IO 贵一到两个数量级,优化器宁可选顺序扫描。所以看到 Index Scan 先别急着骂优化器,要判断它是"合理扫描"还是"该 Seek 却 Scan 的退化"。

区分两者有个实用经验:看 WHERE 条件的过滤性和返回行数。如果查询本来就是要拉取全表三成数据,Scan 合理;如果条件应该过滤到只剩几百行却走了 Scan,那就是退化,需要继续往下排查。

还需要注意一个概念区分:表扫描(Table Scan)和索引扫描(Index Scan)。堆表上没有聚集索引,逻辑读时直接扫堆页,叫 Table Scan;有聚集索引的表扫聚集索引的叶页,叫 Clustered Index Scan;扫非聚集索引的叶页,叫 Index Scan。三者物理操作不同,但影响方向一致——都是把整个结构从头读一遍。执行计划里看到 Clustered Index Scan 时也别忽略,很多人只盯非聚集 Index Scan,忘了聚集索引被扫同样是大问题,因为聚集索引叶页包含全表数据,扫描成本更高。

2.2 SSMS 执行计划三个定位点:操作符、估计行数与警告标志

打开 SSMS,按 Ctrl+M 开启"包含实际执行计划",把慢查询跑一遍。重点看三处:表访问节点显示的是 Index Seek 还是 Index Scan;Estimated Number of Rows(估计行数)与 Actual Number of Rows(实际行数)的偏差;节点上有没有黄色警告三角。警告如果提示"缺少统计信息"或"隐式转换",问题基本直接锁定。

退化场景里最典型的画面是:Estimated Number of Rows 显示 1,Actual Number of Rows 却是几十万行。优化器按统计信息以为返回一条,实际返回一大片,于是选了个代价极低路径。这里有两个方向要分清:一种是统计信息过期导致估计偏差,另一种是参数嗅探复用旧计划。两者症状相似,处理方式不同,后面章节会分别展开。

看执行计划时也可以直接看 XML 形式。右键计划选"显示执行计划 XML",搜 PhysicalOp 节点:

<RelOp PhysicalOp="Index Scan" EstimateRows="1" ActualRows="984621" ...>

这个 XML 片段里 PhysicalOp 是 Index Scan,EstimateRows 是 1,ActualRows 却是 98 万,优化器对返回行数的判断完全失准。ActualRows 属性只在"包含实际执行计划"的会话里写入,光看预估计划看不到这一项。

如果想快速验证"扫描到底比查找慢多少",可以用 FORCESEEK 提示强制优化器走索引:

SELECT OrderId, CustomerId, OrderDate FROM Sales.Orders WITH (FORCESEEK) WHERE CustomerId = 23456;

这个提示让优化器忽略代价估算直接选 Seek,如果强制走 Seek 之后逻辑读从 320 降到 10,说明优化器原本的 Scan 选择是统计信息误导下的错误决定;如果强制走 Seek 后逻辑读反而更高,说明这条查询的数据分布确实适合扫描,优化器没选错。FORCESEEK 是排查手段,不是长期解决办法,生产环境别把它写进正式代码。

2.3 SET STATISTICS IO 验证:扫描的逻辑读到底高在哪

执行计划是静态结构,动态开销要关掉执行计划单独看统计信息。在查询前执行:

SET STATISTICS IO ON; SET STATISTICS TIME ON; GO SELECT OrderId, CustomerId, OrderDate FROM Sales.Orders WHERE CustomerId = 10248; GO

结果里的关键行长这样:表 'Orders'。扫描计数 1,逻辑读 320 次,物理读 0 次。走 Scan 时逻辑读近似等于索引总页数;走 Seek 时逻辑读接近树高加命中页数。同样是这条查询,CustomerId 走 Seek 时逻辑读可能只有 8 次,Scan 时是 320 次,差距一目了然。

参数说明:SET STATISTICS IO 的"扫描计数"代表表或索引在本次查询中被访问的次数,不等于执行了 Index Scan;"逻辑读"是缓冲区中读取的页数,页是 8KB 单位。"物理读"是磁盘读取的次数,首次跑冷数据时才有值,第二次跑基本是 0,因为数据已进内存。会话级别开启后,每一条后续查询都会输出统计信息,测试完记得用 SET STATISTICS IO OFF 关掉。

我一般会把 SET STATISTICS IO 和执行计划结合起来看:先确认是哪类算子,再确认 IO 数量级。如果物理读为 0、逻辑读很高,说明问题出在计划选择本身,而不是磁盘瓶颈。这个区分能帮你把索引问题和存储性能问题分开,省得优化半天索引结果发现是磁盘阵列慢。

3. 五类诱发因素:查询写法、统计信息、参数嗅探与索引设计

3.1 非 SARG 谓词:函数包裹和隐式转换让索引白建

SARG(Search ARGument,可搜索参数)是判断一个谓词能不能用索引的前置条件。能满足 SARG 的写法是"列在比较运算符一边,常量在另一边"。一旦在列上套了函数,优化器就无法用索引定位,只能退化成扫描。

看两个典型翻车写法:

-- 翻车写法 1:函数包裹列,索引失效 SELECT OrderId, TotalDue FROM Sales.Orders WHERE YEAR(OrderDate) = 2024; -- 正确写法:范围谓词,能走索引查找 SELECT OrderId, TotalDue FROM Sales.Orders WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01';

逻辑说明:第一句对 OrderDate 做 YEAR 运算后,索引里的键值和谓词中的值无法直接比较,优化器只能把每一条记录都取出来算一遍,这等价于扫描。第二句把条件改写成半开区间,B 树就能按边界值直接定位。

隐式转换是另一个高频来源。最常见的一种是参数类型和列类型不一致:

-- CustomerId 列是 varchar(10),参数是 nvarchar(30) SELECT * FROM Sales.Customers WHERE CustomerId = @CustomerId;

因为 nvarchar 的优先级比 varchar 高,优化器会把列转换成 nvarchar 再比较,转换后索引查找失效,变成索引扫描。这类坑排查时可以在计划 XML 里搜 CONVERT_IMPLICIT 关键字确认,位置出现在列侧而不是参数侧,就实锤了。

3.2 统计信息过期:基数估计偏离让优化器放弃索引

优化器靠统计信息里的密度、直方图来估计将返回多少行。当统计信息滞后于真实数据分布,基数估计就会严重偏离,代价模型跟着错,选出来的访问路径自然不靠谱。

查看统计信息状态的查询:

SELECT OBJECT_NAME(sp.object_id) AS TableName, s.name AS StatsName, sp.last_updated, sp.rows AS TotalRows, sp.rows_sampled AS SampledRows, sp.modification_counter AS ModifyCount, sp.auto_updated AS AutoUpdated FROM sys.stats s CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp WHERE OBJECT_NAME(s.object_id) = 'Orders';

参数与逻辑说明:dm_db_stats_properties 从 SQL Server 2008 R2 开始可用,返回统计信息对应的元数据快照。last_updated 是最近一次更新时间;modification_counter 是自上次更新以来的数据修改次数。如果 modification_counter 很大而 last_updated 很旧,说明数据变了但统计信息还没触发自动更新,优化器还在拿旧分布做基数估计,这个状态下 Seek 变 Scan 非常常见。

需要特别注意自动更新触发阈值不是"改了 1 行就更新"。对于 500 行以上的表,改动超过总行数的 20% 加 500 行才触发;小表则要求改动超过 500 行。大量增删改后若没达到阈值,统计信息会一直保持旧值。这也是为什么很多 DBA 对核心大表做每周手动更新,而不是完全依赖自动机制。

3.3 参数嗅探与计划重用:坏计划被复用到所有场景

存储过程默认走参数嗅探——第一次执行时优化器基于当时的参数值、统计信息和数据分布生成计划,然后塞进计划缓存,后续所有调用都复用它。如果第一次的参数碰巧是返回行数极小的值,生成的 Seek 计划后来被大范围查询复用,性能就会崩。

验证参数嗅探最直接的办法:清掉该语句的计划缓存再跑一次。

-- 测试环境谨慎操作 DBCC FREEPROCCACHE; GO -- 或者用查询提示强制每次重编译 SELECT OrderId, OrderDate, TotalDue FROM Sales.Orders WHERE CustomerId = @CustomerId OPTION (RECOMPILE);

逻辑说明:DBCC FREEPROCCACHE 会把整个实例的缓存清空,生产上要非常谨慎,测试环境没问题。OPTION (RECOMPILE) 让每次调用都重新编译,彻底避开缓存,但代价是编译开销。另一种折中是 OPTIMIZE FOR (@CustomerId UNKNOWN),让优化器按平均分布编译,不偏向某个参数值。实际维护中我倾向于先用 RECOMPILE 确认是不是嗅探,确认后再定长期策略——如果业务参数分布跨度极大,OPTIMIZE FOR UNKNOWN 往往比 RECOMPILE 更稳,因为后者的编译开销在超高并发下会被放大。

3.4 复合索引键列顺序与 INCLUDE 列:设计失误一样逼出扫描

优化器想走 Seek,前提是索引的键列顺序和查询谓词能对上。复合索引(A, B, C)只能对 A、A+B、A+B+C 这三个前缀做 seek,若查询条件是 B = @b,索引没法定位,只能扫。区分度高的列放前面,是设计复合索引的基本准则。

另一个设计缺陷是覆盖不足。查询要返回的列不在索引键和 INCLUDE 里,走 Seek 后还要做 Key Lookup 回表取列。当返回行数较多时,Seek 加回表的随机 IO 总成本可能高过直接扫描,优化器权衡后放弃 Seek。这种"被迫扫描"和谓词无关,属于覆盖度不足。解决方法是检查 SELECT 列表,把高频返回列加进 INCLUDE,让索引达到覆盖索引的标准。

还有一种常见误用:在低区分度列上单独建索引。比如性别列只有两个值,优化器知道这个索引选择性极差,几乎不会选 Seek。这不是索引坏了,是设计时就没考虑选择性。遇到这种情况,与其纠结为什么没有 Seek,不如把该列从独立索引里拿掉,和别的列组成复合索引。

4. 用三组 DMV 排查根因:索引级统计到语句级证据链

4.1 查询 sys.dm_db_index_usage_stats 找出从未被 Seek 的索引

运维一个库里动辄几十上百个索引,逐个验证不现实。拿这个 DMV 先把"重扫描、零查找"的索引揪出来:

SELECT OBJECT_NAME(i.object_id) AS TableName, i.name AS IndexName, i.type_desc, u.user_seeks, u.user_scans, u.user_lookups, u.user_updates FROM sys.dm_db_index_usage_stats u JOIN sys.indexes i ON u.object_id = i.object_id AND u.index_id = i.index_id WHERE u.database_id = DB_ID('YourDatabase') AND i.type_desc = 'NONCLUSTERED' ORDER BY u.user_scans DESC;

逻辑说明:user_seeks 是该索引被用于 Index Seek 的次数,user_scans 是扫描次数,user_lookups 是 Key/RID Lookup 次数。user_seeks 为 0 而 user_scans 很高的非聚集索引,要么是覆盖索引常被整体扫描当覆盖用,要么就是长期在做无效扫描——两种情况都值得继续接入语句级分析。

另一个顺手可以查的视角是缺失索引 DMV:

SELECT migs.avg_total_user_cost, migs.avg_user_impact, mid.statement AS TableName, mid.equality_columns, mid.inequality_columns, mid.included_columns FROM sys.dm_db_missing_index_group_stats migs JOIN sys.dm_db_missing_index_groups mig ON migs.group_handle = mig.index_group_handle JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle WHERE mid.database_id = DB_ID('YourDatabase') ORDER BY migs.avg_user_impact DESC;

如果缺失索引建议里的 equality_columns 和业务 WHERE 条件完全吻合,说明优化器一直在扫描这个表——因为根本找不到合适的索引。当然缺失索引 DMV 只是优化器编译时的建议,不代表建了就一定走 Seek,还要拿语句级的实际执行计划验证。

4.2 用 sys.dm_exec_query_stats 定位高逻辑读的扫描语句

索引级统计只能说明"这索引被扫过",具体哪条 SQL 在扫,还得查语句级 DMV:

SELECT TOP 20 SUBSTRING(st.text, (qs.statement_start_offset / 2) + 1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1) AS query_text, qs.execution_count, qs.total_logical_reads, qs.total_logical_reads / qs.execution_count AS avg_logical_reads, qs.total_elapsed_time / qs.execution_count AS avg_elapsed_us FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st ORDER BY avg_logical_reads DESC;

参数说明:statement_start_offset 和 statement_end_offset 是文本在批处理中的偏移字节,除以 2 是因为 SQL 文本按 Unicode 存储。total_logical_reads 除以 execution_count 得到单次平均逻辑读,比只看总量更能反映常态。把 TOP 20 的结果和 4.1 里的索引名对起来,基本能锁定产生扫描的语句。

如果想直接过滤"执行计划里含 Index Scan 的缓存语句",加上对计划 XML 的 XQuery 判断:

SELECT TOP 10 SUBSTRING(st.text, (qs.statement_start_offset/2)+1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS query_text, qs.execution_count, qs.total_logical_reads/qs.execution_count AS avg_logical_reads FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp WHERE qp.query_plan.value('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; count(//p:RelOp[@PhysicalOp="Index Scan"])', 'int') > 0 ORDER BY qs.total_logical_reads DESC;

这个查询用 XQuery 的 count 统计计划 XML 里 Index Scan 算子的个数,把包含扫描算子的语句直接筛出来。注意命名空间 URI 里的 2004/07 是 showplan 的固定标识,不是 SQL Server 版本号,2008 到 2022 都一样。

4.3 用重编译验证参数嗅探:清缓存前后对比执行计划

前面提过 FREEPROCCACHE,这里给一套更可控的验证流程。先用下面语句拿到某个 SQL 的 plan_handle,再只清这一条:

-- 找到目标语句的 plan_handle SELECT plan_handle, execution_count FROM sys.dm_exec_query_stats CROSS APPLY sys.dm_exec_sql_text(sql_handle) WHERE text LIKE '%SELECT OrderId, TotalDue%'; -- 只清这一个句子的计划 DBCC FREEPROCCACHE (@plan_handle);

逻辑说明:执行 DBCC FREEPROCCACHE 之后再跑一次原查询,如果执行计划里的 Index Scan 变成 Index Seek,那基本可以判定是计划缓存里存了一个坏的旧计划。这个验证很关键,因为统计信息过期和参数嗅探的症状相似,但修法完全不同。用重编译隔离掉缓存因素后,再判断统计信息是否需要更新,能省掉很多瞎折腾的时间。

提示:DBCC FREEPROCCACHE 在生产环境属于高危操作,清缓存会导致后续所有查询瞬间重编译,CPU 会有一个明显尖峰。能定位到单条语句就用单条清理,不要图省事全库清。

一个真实场景:销售报表存储过程每天第一次跑要 40 秒,第二次跑只要 3 秒。第一次跑时优化器基于空缓存编译,参数是 0(查全部),编译出扫描计划;第二次查询走计划缓存,一执行却发现要返回 40 万行,运行时又调整成别的计划。用上面方法清掉缓存后,用大参数值重新编译,才能看到真正的 Seek 计划长什么样。这类场景在报表类存储过程里出现频率极高,因为报表参数默认值往往就是"全量"。

5. 避坑:索引查找变索引扫描的 5 条血泪教训

5.1 统计信息更新了但执行计划没刷新:顺序反了等于白做

现象:手动执行 UPDATE STATISTICS 后,慢查询依旧走扫描,执行计划看起来和更新前一模一样。

原因:统计信息更新不会自动驱逐相关执行计划。只要计划还在缓存里,后续查询仍然复用旧计划,旧计划里写死的基数估计还是过期值。

解决:更新统计后对目标语句做一次重编译。用 sp_recompile 或 DBCC FREEPROCCACHE(plan_handle) 清掉对应计划,让优化器按新统计重新编译。这个顺序必须是先更新统计,再清计划,顺序反了等于白做——先清计划再更新统计,新计划会基于旧统计生成,更新又可能改变计划,最后可以查一下 sys.dm_exec_query_stats 确认计划重建时间。

5.2 隐式转换:varchar 列配 nvarchar 参数

现象:表结构里 CustomerId 是 varchar(10),应用程序传参用 string 默认映射成 nvarchar,SQL 里写 @CustomerId,结果索引列被隐式转换成 nvarchar,Index Seek 变 Index Scan。逻辑读从几十涨到几千。

原因:数据类型优先级里 nvarchar 高于 varchar,SQL Server 会转换低优先级一侧,转换发生在列上就破坏了 SARG 属性。

解决:要么把应用参数类型显式指定为 DbType.AnsiString,要么把表列改成 nvarchar 统一字符类型。改之前想清楚这个列未来是否要存生僻字符,一次性统一的成本远低于长期踩坑。排查时在计划 XML 里搜 CONVERT_IMPLICIT,如果出现的位置在列侧而不是参数侧,基本实锤。

5.3 自动更新统计信息的触发阈值比你想象的迟钝

现象:一张大订单表夜里批量导入了 30 万行,早上业务查询从毫秒变秒级,执行计划里 Estimated Rows 还是 1,Actual Rows 却是三十万。

原因:自动更新对 500 行以上的表要求修改超过总行数的 20% + 500 行。表有 100 万行,改动要超过 20.5 万才触发;而且触发的自动更新是异步的,即使达到阈值,当前这次编译也可能来不及用上新统计。

解决:对批量导入这类业务,维护窗口里手动更新统计信息,或者在导入结束后自动执行一次 UPDATE STATISTICS。用下面的脚本按表粒度批量刷新:

EXEC sp_MSforeachtable 'UPDATE STATISTICS ? WITH FULLSCAN';

参数说明:FULLSCAN 表示全量采样,最慢但最准确。大表可以选择 SAMPLE 5 PERCENT 这类抽样方式节省时间,关键是让抽样比例与数据分布匹配——均匀分布的数据抽样 5% 够用,偏斜严重的列最好 FULLSCAN。

5.4 LIKE 前置通配符让任何索引都白搭

现象:WHERE Name LIKE '%Server%' 这种查询,不管 Name 上有没有索引,永远走 Index Scan。

原因:前置通配符导致无法确定搜索起点,B 树查找的前提是能界定区间。尾部通配符 LIKE 'Server%' 可以走 Seek,因为起点可确定。

解决:业务允许的情况下把参数约束到"以某字开头",用 LIKE 'Server%'。真正需要任意位置子串检索的场景,用 SQL Server 全文索引,或者直接把检索逻辑放到搜索引擎/内存数据库,别硬在 B 树上抠。全文索引的维护成本和磁盘占用都不小,但它是这类需求的正解。

5.5 复合索引列顺序写反,优化器想帮都帮不上

现象:表上建了索引(CustomerId, OrderDate),业务查询是 WHERE OrderDate = @d AND CustomerId = @c。如果把索引建成(OrderDate, CustomerId),而查询以 CustomerId 为主导,就只能扫描。

原因:复合索引只能按前缀利用。查询谓词里如果没有索引第一个键列的可命中条件,后面的键列即使匹配也派不上用场。

解决:把查询里最常出现、区分度最高的等值谓词列放在复合索引第一位。这也是为什么设计索引时要先收集业务真实查询,而不是按表结构的字段顺序盲目建。用数据库引擎优化顾问(DTA)或手工分析 WHERE/JOIN 列的出现频率,都能降低这类配置失误。

6. 回归验证与维护策略:让 Seek 长期稳住的三道工序

6.1 优化前后同一参数、同一数据量下对比执行计划

改完索引或查询后,最怕"这次快了"是内存缓存假象。同一批数据、同一个参数值、同一台机器上,先清相应计划缓存再分别跑优化前和优化后的语句,对比三个数字:执行计划里是 Seek 还是 Scan、逻辑读变化、SELECT 返回行数与 Estimated Rows 的偏差。把两次的计划 XML 存下来做对比,重点看 PhysicalOp 节点和 EstimateRows 属性,差值越小说明优化器的判断越准。

6.2 统计信息更新策略:自动与手动配合

对大表维护一个固定任务,每周或每日对变动较大的核心表执行:

UPDATE STATISTICS Sales.Orders(IX_Orders_CustomerId_OrderDate) WITH SAMPLE 20 PERCENT;

SAMPLE 20 PERCENT 表示按 20% 的行采样计算分布,比 FULLSCAN 快但精确度略低;对核心大表建议 FULLSCAN,对一般表采样足够。这个频率从业务数据量出发,不要照抄模板。更新完立刻回归 6.1 的对比,确认统计变化没有把 Seek 计划改成 Scan——统计信息更新本身也会改变计划,更新成坏计划的情况同样存在。

6.3 索引碎片管理:重建与重组的边界

碎片本身通常不会让 Seek 变 Scan,但会让扫描的实际开销更高,也会让 Seek 的随机 IO 变慢。常见的维护习惯是:碎片率低于 5% 不管;5%~30% 做 ALTER INDEX REORGANIZE;超过 30% 做 REBUILD。REBUILD 大索引时会锁表,SQL Server 2008 到 2022 各版本对在线重建的支持不一样,操作前确认版本和 Edition,企业版在线重建才可用。

我自己踩过的坑是:一开始出了慢查询就急着重建索引,操作完当时是快了,但过几天又退化。后来发现根因是统计信息过期,却还有查询写法里的隐式转换没解决。先建一个监控脚本,把这四类信号按周收集成一张表——语句逻辑读、索引 user_seeks/user_scans、统计信息 modification_counter、计划缓存里的重编译次数——出现异常时再针对性处理,而不是看到 Scan 就开重建。这套思路在 SQL Server 2016 到 2022 上都适用,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询