本节摘要:一条 T-SQL 从客户端发出到结果集返回,要经历解析、绑定、优化、缓存判定、执行五个阶段。本节逐段拆解这条流水线,重点讲优化器的分级搜索策略(为什么有的计划一眼即得、有的要精打细算)与计划缓存的复用规则。这是排错的总框架——后面所有"为什么这么慢"的追问,都要先回答"它在哪个阶段出了问题"。
把一条 SELECT 当作一个旅客,它的旅程有五站。第一站解析:命令解析器检查语法,把文本变成语法树——语法错在这一站就被退回,快得几乎测不到延迟。第二站绑定:代数优化器把表名、列名绑定到实际的目录元数据,检查对象存在性与类型兼容性,产出逻辑树;"对象名无效"这类错误在这一站爆出。第三站优化:查询优化器拿逻辑树与统计信息,穷举可行的物理计划并按成本选优。第四站缓存判定:注意,真正的顺序是先查缓存——同一条参数化 SQL 若已有缓存计划,前三站产出的结构可以复用判定,直接跳过昂贵的优化。第五站执行:执行器把计划交给存储引擎逐算子拉取数据,结果集流回客户端。
这个顺序里藏着排错的第一把钥匙:先判断慢在哪一站。语法与绑定阶段的问题一秒暴露;缓存命中却依然慢,问题在计划本身或资源层;缓存未命中且编译耗时高,是编译风暴;执行阶段慢,才轮到第 8 章的资源取证。上来就翻索引、调参数,等于没问病史先开药。
优化要花时间,复杂查询的可行计划数是天文数字,穷举不现实。SQL Server 的优化器用分级搜索:先看这条查询有没有"唯一解"——像 SELECT COUNT 这种没有优化空间的小查询,直接给平凡计划,一微秒完事;有一定复杂度的进第一阶段搜索,用简单规则试常见形态(比如单表索引选择),找到成本够低的计划就收工;还不行的进深度搜索,做连接顺序重排、并行计划探索等全套运算。实践含义有两条:其一,简单查询的编译开销可以忽略,别为它们做任何干预;其二,超复杂查询(几十个连接的大报表)编译本身就可能是秒级开销,它们的优化质量与编译时间都值得单独关注。
-- 从缓存里看一条计划被复用了多少次、编译花了多少 CPU SELECT TOP 10 qs.execution_count AS 执行次数, qs.total_worker_time / 1000 AS 总CPU毫秒, qs.total_elapsed_time / qs.execution_count / 1000 AS 平均耗时微秒, st.text AS 语句文本, qp.query_plan AS 计划XML 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 ORDER BY qs.execution_count DESC; -- 执行次数上万、平均耗时却高的条目:计划在疯狂复用,但复用的可能是个坏计划。
缓存键的核心是查询文本加数据库上下文等要素。参数化的查询(应用端用了参数绑定)只有一份计划供所有参数复用,这既省内存又省编译;非参数化的拼接 SQL 每种字面量组合都是新文本,各编各的计划,缓存被冲爆的同时挤占数据缓存——第 2 章讲过的"计划膨胀"。另一面是参数嗅探:首次编译用首见参数的统计特征选计划,这个计划此后被所有参数共享。当参数的选择性差异巨大(有的客户三笔订单、有的三万笔),首见参数恰好是小户,缓存的查找计划对大户就是灾难——这正是第 3 章图 4-1 案例的标准成因。
治理手段按侵入度排序:查询里对敏感语句加 OPTION RECOMPILE,每次执行重新编译,用编译换计划贴合;应用端避免把高偏斜参数的查询写成单语句热点;2019 版之后可开参数敏感计划优化,让缓存为坏参数保留补救计划;再往上是用查询存储锁定好计划强制映射。每一档的取舍(编译成本、计划贴合度、运维复杂度)在 4.2 与 8.3 的方法论里还会反复权衡。
收到"查询时快时慢"的工单,按生命周期走判定流:若同一条 SQL 有多份相似计划在缓存里(文本略有差异),先怀疑拼接 SQL 与计划膨胀,推动参数化;若只有一份计划但执行指标两极分化,锁定参数嗅探,取坏参数样本对比估计行数与实际行数;若计划本身没毛病、执行时等待资源(IO、锁),交回第 2 章与第 5 章的框架。三步走完,八成"时快时慢"案件都能归入三类之一:膨胀、嗅探、资源。
生命周期框架下的另一类高发病是编译风暴:短时间内海量查询同时进入优化阶段,CPU 被编译吃满,计划缓存抖动。取证看两个证据:缓存目录里"单次执行"计划的数量与占比(对象类型为即席的条目堆积),以及统计信息自动更新事件在时间轴上的聚集。缓解分层:应用侧推动参数化(治本);实例侧打开即席负载优化,让只跑一次的计划先存编译骨架、重复执行才存全量计划,缓存占用立降一个量级;极端场景可考虑强制参数化,让字面量也共享计划——但它的副作用是扩大参数嗅探面,高偏斜负载慎用。三个层次按序尝试,多数风暴停在第一层就解决了。
还有一个反直觉的预防项:统计信息的异步更新选项。默认同步更新意味着"查询触发过期统计,先更新再编译",高峰期成百上千个会话排队等同一次更新,风暴由此放大;改异步后,查询用旧统计先跑、后台更新完供后续使用——牺牲个别查询的计划精度,换来高峰期的平稳。交互式交易库值得评估这个取舍。
生命周期框架搭好了。下一节带着这个框架上一线:打开执行计划,认识那些方块与箭头,用一个真实案例把"读计划三步法"练成手艺。