SQL Server 执行计划怎么看?快速定位 SQL 慢的根源
90% 的 SQL 性能问题,看一眼执行计划就能找到原因。本文不讲废话,直接告诉你怎么看、看哪里、哪些是红色警报。
一、为什么一定要学会看执行计划?
SQL Server 真正执行的,不是你写的 SQL,而是优化器生成的执行计划。
-
同一条 SQL,数据量变化 → 执行计划可能改变
-
索引变了 → 执行计划会变
-
统计信息过期 → 执行计划会错
👉 不看执行计划,调优全靠猜。
二、如何打开执行计划?
✅ 方式一:SSMS(最常用)
在 SSMS 中:
-
实际执行计划 :
Ctrl + M -
预估执行计划 :
Ctrl + L
建议:调优时一定用 实际执行计划,它包含真实行数、实际耗时。
✅ 方式二:SET 命令(脚本友好)
SET STATISTICS PROFILE ON;
GO
SELECT ...
GO
SET STATISTICS PROFILE OFF;
✅ 方式三:Plan Cache(线上排查利器)
SELECT TOP 10
qs.total_elapsed_time / qs.execution_count AS avg_duration_ms,
qs.execution_count,
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 sql_text,
qp.query_plan
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 avg_duration_ms DESC;
✅ 适合:线上慢 SQL 抓现行
三、执行计划怎么读?(从上往下,从右往左)
核心阅读顺序
从右向左(数据来源)
↓
从上向下(执行顺序)
很多人一上来就盯着箭头粗细,其实算子类型比箭头更重要。
四、必须警惕的 7 个"红色信号"
下面这些算子,出现一个就要警惕,出现两个基本可以确定慢的根源。
🚨 1. Table Scan(表扫描)------最致命
!Table Scan
含义:没有合适的索引,一行一行扫全表。
✅ 解决:
-
在 WHERE / JOIN 字段上建索引
-
检查索引是否被禁用
❌ 常见原因:
-
没建索引
-
索引列被函数包裹
-
隐式转换(VARCHAR ↔ NVARCHAR)
🚨 2. Clustered Index Scan(聚集索引扫描)
虽然用了聚集索引,但本质是全表扫描。
✅ 解决:
-
优化 WHERE 条件
-
考虑非聚集索引 + 覆盖索引
🚨 3. RID Lookup / Key Lookup(回表)
含义:非聚集索引没覆盖查询字段,需要回到表中拿数据。
-- 原查询
SELECT Name, Age FROM Users WHERE Email = 'a@b.com';
-- 索引
CREATE INDEX IX_Email ON Users(Email); -- 缺 Name, Age
✅ 解决:
CREATE INDEX IX_Email
ON Users(Email)
INCLUDE (Name, Age);
✅ Lookup 次数 ≈ 返回行数,几万次 Lookup 就是性能灾难。
🚨 4. Sort(排序)------CPU 杀手
含义:内存中排序,内存不够就 Spill to TempDB。
✅ 解决:
-
在 ORDER BY 字段上建索引
-
减少排序字段数量
-
确认是否真的需要排序
⚠️ 看到 Sort Warnings,说明已经用到了磁盘排序。
🚨 5. Hash Match(哈希匹配)
常见于:
-
大表 JOIN
-
GROUP BY
✅ 特点:
-
内存消耗大
-
数据量大时容易溢出到 TempDB
✅ 优化方向:
-
小表驱动大表
-
JOIN 字段建索引
-
避免不必要的 GROUP BY
🚨 6. Nested Loops(嵌套循环)+ 高行数
外层循环一次,内层循环 N 次。
✅ 适合:小结果集
❌ 不适合:大表 JOIN
👉 如果 Nested Loops 的"外部输入"是大表,基本必慢。
🚨 7. Parallelism(并行)------双刃剑
并行本身不是坏事,但:
-
CXPACKET 等待过高
-
并行开销占比过大
✅ 解决:
-
优化索引,减少数据量
-
调整
MAXDOP -
检查统计信息
五、箭头粗细 ≠ 一切,但这几个指标更关键
点击任意算子,看 属性窗口(F4):
| 指标 | 含义 | 危险信号 |
|---|---|---|
| Estimated Number of Rows | 预估行数 | 与 Actual 差距巨大 |
| Actual Number of Rows | 实际行数 | 远大于预估 |
| Estimated Cost | 预估成本 | 某一步占比 > 50% |
| Memory Grant | 内存授予 | 过大或溢出 |
| I/O Cost | IO 成本 | 远高于 CPU Cost |
预估 vs 实际行数差异巨大 = 统计信息过期
六、30 秒快速定位慢 SQL 的实战流程
Step 1:看最耗成本的算子
-
哪个算子占比最高?
-
是不是 Scan / Sort / Hash Match?
Step 2:看数据从哪里来
-
Table Scan?
-
Clustered Index Scan?
-
有没有走索引?
Step 3:看是否有回表
-
Key Lookup / RID Lookup 是否存在?
-
能否改成覆盖索引?
Step 4:检查排序和聚合
-
Sort 是否可避免?
-
GROUP BY 字段是否有索引?
Step 5:检查隐式转换
在执行计划中搜索:
CONVERT_IMPLICIT
一旦出现,立刻检查字段类型是否一致。
七、一个完整的实战案例
原始 SQL
SELECT o.OrderId, o.CreateTime, c.Name
FROM Orders o
JOIN Customers c ON o.CustomerId = c.Id
WHERE o.Status = 1
AND o.CreateTime >= '2024-01-01'
ORDER BY o.CreateTime;
执行计划暴露的问题
-
Clustered Index Scan on Orders -
Sort on CreateTime -
Key Lookup on Customers
优化后索引
-- Orders 表
CREATE INDEX IX_Orders_Status_CreateTime
ON Orders(Status, CreateTime)
INCLUDE (OrderId, CustomerId);
-- Customers 表
CREATE INDEX IX_Customers_Id
ON Customers(Id)
INCLUDE (Name);
✅ 结果:
-
Scan → Seek
-
Sort 消失
-
Lookup 消失
-
耗时从 2.3s → 12ms
八、常见执行计划误解澄清
| 误解 | 事实 |
|---|---|
| 索引 Scan 一定慢 | 小表、全表访问反而更快 |
| Seek 一定快 | 高频率 Seek + Lookup 可能更慢 |
| 并行一定好 | 并行有协调成本 |
| 执行计划不变 | 参数嗅探会改变计划 |
九、进阶:用 DMV 验证你的判断
查看缺失索引建议
SELECT *
FROM sys.dm_db_missing_index_details;
⚠️ 不要照单全收,要结合业务判断。
查看统计信息是否过期
SELECT
name,
auto_created,
stats_date(object_id, stats_id) AS last_updated
FROM sys.stats
WHERE object_id = OBJECT_ID('Orders');
十、一句话总结
执行计划不是让你"看懂",而是让你"发现问题"。
记住这个公式:
慢 SQL = 扫描 + 回表 + 排序 + 错误的 JOIN 策略