3. 执行计划入门
执行计划是 SQL Server 查询优化器在收到一条 T-SQL 语句后,经过语法分析、代数化、基于开销的优化等阶段,最终产出的“执行蓝图”。它描述的不是“结果是什么”,而是“引擎打算怎样一步步得到结果”:先访问哪张表、用哪个索引、以什么顺序连接、在哪里排序、在哪里聚合、数据在算子之间如何流动。学会阅读执行计划,等于拿到了观察查询性能的显微镜——你能看到慢在哪里、为什么慢、优化器做了哪些取舍。
SQL Server 提供两种主要的执行计划形态:图形化执行计划(SSMS 中 Ctrl+M 后执行,或 Ctrl+L 预估)和文本执行计划(SET SHOWPLAN_ALL / SHOWPLAN_XML / STATISTICS PROFILE 等)。图形化计划直观、适合快速定位热点;文本计划信息密度高、便于对比和自动化分析。二者本质是同一棵算子树的不同呈现,理解其一即可迁移到另一。
执行计划是一棵从右向左、由内向外读的算子树
图形化执行计划中,数据流方向是从右向左:最右侧的叶子节点是数据访问算子(扫描、查找),中间是连接、排序、聚合等处理算子,最左侧是最终结果返回。箭头代表行集(rowset)在算子间的传递。每个算子框上通常显示三个关键数字:预估行数(Estimated Number of Rows)、预估 I/O 开销和预估 CPU 开销,三者共同构成该算子的预估总开销,也是优化器选择该计划的依据。
文本计划(SHOWPLAN_XML)则以 XML 层级表达同一结构,每个 RelOp 节点带有 PhysicalOp、LogicalOp、EstimateRows、EstimatedTotalSubtreeCost 等属性。图形化计划中鼠标悬停看到的“属性”窗口,正是这些 XML 属性的可视化映射。因此,读图形计划时养成“看算子类型 + 看预估行数 + 看实际行数”的习惯,就能抓住绝大多数性能线索。
核心算子:扫描、查找、连接、排序
扫描(Scan)包括 Table Scan、Clustered Index Scan、Index Scan。它意味着引擎需要遍历整张表或整个索引结构来找到满足条件的行。扫描本身不是错误——当查询需要返回表中大部分数据时,扫描反而是最高效的选择。问题在于:当查询只应返回少量行,却出现大表扫描时,通常说明缺少合适索引,或索引未被有效使用。判断依据是“预估行数”与“表总行数”的比例,以及该算子的开销占比。
查找(Seek)包括 Clustered Index Seek、Index Seek、Key Lookup、RID Lookup。Seek 通过 B 树导航直接定位到满足谓词的行,代价与返回行数近似成正比,而非与表大小成正比。Key Lookup / RID Lookup 是“回表”操作:当非聚集索引不覆盖查询所需列时,引擎先通过 Seek 找到索引键,再回到聚集索引或堆中取剩余列。大量 Key Lookup 往往提示需要覆盖索引或调整索引包含列。
连接(Join)主要有三种物理算子:Nested Loops Join 适合外表行数少、内表有高效 Seek 的场景;Hash Match Join 适合中大结果集等值连接,需要内存建哈希表,内存不足会溢出到 tempdb;Merge Join 要求两侧输入已按连接键排序,适合大表且有序的场景。优化器根据预估行数、统计信息和开销模型选择连接方式,读计划时要关注连接算子两侧的预估行数是否悬殊,以及是否出现“Hash Match 溢出”警告。
排序(Sort)通常由 ORDER BY、GROUP BY、DISTINCT 或 Merge Join 前的有序要求触发。排序是阻塞算子:必须等所有输入行到齐才能输出第一行,且可能占用大量内存甚至溢出到 tempdb。如果排序开销占比高,可考虑通过索引消除排序,或检查是否真的需要全局排序。
实际观察:用 STATISTICS IO 与执行计划交叉验证
仅看预估计划不够,实际执行计划(Ctrl+M 后执行)会显示“实际行数”和“实际执行次数”。当预估行数与实际行数差异巨大时,说明统计信息过期或参数嗅探导致基数估计错误,优化器可能因此选错连接方式或访问路径。配合 SET STATISTICS IO ON 可以查看逻辑读、物理读,与执行计划中的扫描/查找算子相互印证。
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT c.CustomerID, c.CustomerName, SUM(o.Amount) AS TotalAmount
FROM dbo.Customers AS c
JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID
WHERE o.OrderDate >= '2024-01-01'
GROUP BY c.CustomerID, c.CustomerName
ORDER BY TotalAmount DESC;
-- 输出示例(合理假设):
-- Table 'Orders'. Scan count 1, logical reads 1250, physical reads 12
-- Table 'Customers'. Scan count 1, logical reads 45, physical reads 0
-- SQL Server Execution Times: CPU time = 156 ms, elapsed time = 210 ms
若上述查询的执行计划中 Orders 表出现 Clustered Index Scan 且预估行数很大,而实际只返回少量行,就应检查 OrderDate 列上是否有索引、统计信息是否更新。若连接算子为 Hash Match 且实际行数远小于预估,可尝试更新统计信息或使用 OPTION (RECOMPILE) 观察计划是否改变。
- 先看最右侧叶子节点:是 Scan 还是 Seek?预估行数多少?
- 再看开销占比最高的算子:它是不是排序、哈希连接或大表扫描?
- 最后对比预估行数与实际行数:差异超过一个数量级,优先怀疑统计信息或参数嗅探。