本节摘要:执行计划是优化器写给你的判决书,也是慢查询排错的第一现场。本节先给出读计划的三步法与关键运算符词典,然后用一个完整的生产案件——报表查询从两秒劣化到四分钟——演示"背景、取证、干预、复盘、变式"的全程办案,最后交代查询提示与查询存储两个干预工具的正确分寸。
图形化执行计划从左往右是数据流出的方向,但办案要从结果倒着回溯:先找成本占比最高的算子,再审它的行数与告警图标,最后把估计行数与实际行数对账。运算符不需要全背,这十来个覆盖九成案件:扫描与查找——扫描是把表或整个索引读一遍,查找是按 B 树精准下探;计划里"本该查找却在扫描",先查谓词是否可 SARG(对列做了函数包裹就废了)。三种连接——嵌套循环适合外表小内表有索引的精准打击,哈希适合大批量无序数据的一次性对撞,归并要求双方按连接键有序、适合超大且已排序的输入;优化器选错连接方式的常见原因是行数估计错得离谱。排序与溢写——排序需要内存授予,给不足就溢写到 tempdb,计划里出现黄色警告;这是报表查询慢的经典病因。键查找——非聚集索引找到行再回表,行数一大不如直接扫描,配合覆盖索引消灭。并行与交换——多核分工的通道算子,行数估计错会让并行变成负资产。
SET STATISTICS IO, TIME ON; -- 打开这条开关再跑慢查询: -- 表 '订单明细'。扫描计数 1,逻辑读 2854332 次,物理读 4120 次…… -- 逻辑读百万级起步的查询,优化方向几乎总是索引与写法。 SET STATISTICS IO, TIME OFF;
背景:月末报表"订单毛利率汇总"原本两秒跑完,某月中旬起劣化到四分钟,业务方已两次升级工单。取证:开启实际执行计划与统计信息重跑。计划显示:订单明细表走了聚集索引扫描(估计行数与实际一致,都是五百万,合理);客户表走了嵌套循环的内侧索引查找——估计 1 行,实际 3000 行。对账发现第一个关键偏离:优化器以为客户筛选条件命中一个客户,实际命中三千个。于是循环被驱动了三百万次,每次都是一次回表随机读。为什么估计 1 行?打开统计信息的直方图,发现上次更新停留在两个月前——新客户的数据全不在直方图里,密度信息把选择性估到了天花板的乐观值。干预:先更新该表统计信息,重新编译后优化器改选哈希连接加并行扫描,耗时降到十一秒;再给订单明细表建按状态列的过滤索引,把十一秒压到两秒三。复盘:根因不是索引缺失而是统计过期,索引只是顺手的优化。查询上溯,找到统计过期的机制原因:该表每天凌晨全量重建(每次重建自动更新统计),但月中业务加了一个只插入的大批量任务,插入路径不触发统计更新阈值——旧规则的盲区。补了一条每周全量更新统计的作业收尾。变式:如果统计及时更新后依然慢,下一站查参数嗅探(对比不同参数下的计划形状);如果计划形状稳定但物理读高,转第 2 章的缓冲池取证;如果只有特定会话慢,查会话设置差异(隔离级别、兼容级别)。
有时你比优化器更懂业务,需要人工干预,工具按侵入度分三档。查询提示最直接:OPTION 里指定连接方式、最大并行度、重编译等。分寸是"提示是绷带不是假肢"——它锁死行为,数据分布变化后反而成为新瓶颈,只用于急救与已验证的特例。计划指南对改不了代码的场景有价值:不改应用 SQL,让服务器在匹配到特定语句时自动附加提示;代价是隐藏逻辑,必须留档。查询存储是现代答案:自动记录每条查询的历史计划与性能指标,遇到"新计划劣化"一键回退到旧计划(计划强制),等于给计划缓存装了黑匣子加回退按钮。2016 版之后的新实例没有理由不开它。
-- 查询存储三件套:开启、找劣化、强制回退 ALTER DATABASE 销售库 SET QUERY_STORE = ON; ALTER DATABASE 销售库 SET QUERY_STORE (OPERATION_MODE = READ_WRITE, MAX_STORAGE_SIZE_MB = 2048); SELECT q.query_id, r.runtime_stats_interval_id, r.avg_duration, r.count_executions FROM sys.query_store_runtime_stats r JOIN sys.query_store_plan p ON p.plan_id = r.plan_id JOIN sys.query_store_query q ON q.query_id = p.query_id ORDER BY r.avg_duration DESC; -- 从劣化时段找出 query_id 与 plan_id 后: EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 7; -- 一句话把好计划按回去,先止血再慢慢查统计信息。
💡 判断该不该用提示的一个土办法:把提示写在便签上贴工位,每季度盘点一次——还在贴着的每一条,都应该升级成查询存储强制计划、索引改造或统计信息修复这类"结构性方案",绷带不许穿成常服。
图形界面之外,计划的可扩展标记语言(XML)版本藏着三处图形视图不易呈现的信息。第一处是缺失索引元素:它列出优化器"认为"缺的索引与预期收益——但切记它是当前查询视角的局部建议,照单全收会造成索引泛滥,正确用法是收集一段时间后按表归并、人工评审。第二处是参数编译值:计划 XML 里记录着编译时实际使用的参数值,参数嗅探案件里"哪个首见参数定了坏计划"一查便知,新版本还记录了运行时值,首见与运行值的对比直接揭示嗅探。第三处是告警节点:溢写、隐式转换、交换分区警告都以告警元素存在——隐式转换告警尤其值得关注,它意味着列类型与比较值类型不一致,索引因此失效,修复往往只是一次类型对齐。
把这三处纳入 4.2 的读计划三步法之后,形成第四步:翻 XML 对细节。前三步定性,第四步定案——尤其在"计划看起来合理但就是慢"的场景,告警元素几乎总能给出答案。
判决书会读、案件能破了。但优化器判决的证据——统计信息与基数估计——本身是怎么造出来的?下一节钻进这台"估数机器"的内部。