数据库优化的难点从来不在“知道概念”,而在于“如何实践”。这篇文章不讲虚的,每个优化方式都提供:
- 问题 SQL(低性能)
- 优化后的 SQL(高性能)
- 使用到的索引
- 原因解释
无论是订单系统、后台管理系统还是 BI 统计,都能直接套用。
一、避免全表扫描:为过滤字段建立合适的索引
❌ 问题示例:WHERE 条件无索引导致 Table Scan
SELECT OrderId, UserId, Amount
FROM Orders
WHERE UserId = 10001;
如果 UserId 没有索引,执行计划会是 Table Scan,随着数据量增加性能急剧下降。
✅ 解决方式:增加非聚集索引
CREATE INDEX IX_Orders_UserId
ON Orders(UserId);
🚀 优化后的查询
SELECT OrderId, UserId, Amount
FROM Orders
WHERE UserId = 10001;
SQL Server 会使用 Index Seek(高性能),查询速度从毫秒级下降到微秒级。
二、避免 Key Lookup:使用包含列让索引“覆盖查询”
❌ 问题示例:索引字段不够,导致大量回表
SELECT OrderId, UserId, Amount
FROM Orders
WHERE UserId = 10001;
如果索引只有 UserId,那 SQL Server 会:
- Seek 找出 UserId 对应记录
- 回表(Lookup)用于读取 Amount 字段
大量回表会造成严重 IO 开销。
❌ 原有索引
CREATE INDEX IX_Orders_UserId
ON Orders(UserId);
✅ 解决方式:使用 INCLUDE 构建覆盖索引
CREATE INDEX IX_Orders_UserId
ON Orders(UserId)
INCLUDE (OrderId, Amount);
🚀 优化后的效果
查询完全在索引内完成,不再回表,执行计划 Key Lookup 消失。
三、避免在字段上使用函数:保持可 SARGABLE(可利用索引)
❌ 问题示例:对字段使用函数,导致索引失效
SELECT *
FROM Orders
WHERE CONVERT(VARCHAR(10), CreateTime, 120) = '2025-12-10';
执行计划会出现:
- Index Scan
- 甚至 Table Scan
因为 SQL Server 无法使用时间字段上的索引。
❌ 原有索引
CREATE INDEX IX_Orders_CreateTime
ON Orders(CreateTime);
✅ 优化方式:改用范围查询
SELECT *
FROM Orders
WHERE CreateTime >= '2025-12-10'
AND CreateTime < '2025-12-11';
此时索引恢复有效,使用 Index Seek。
四、避免 SELECT *:减少 IO,提升覆盖率
❌ 问题示例:滥用 SELECT * 导致性能下降
SELECT *
FROM Orders
WHERE Status = 1;
问题:
- 改变返回列会改变执行计划
- 无法利用覆盖索引
- IO 成本高
❌ 原有索引
CREATE INDEX IX_Orders_Status
ON Orders(Status);
当你 SELECT * 时,需要返回所有字段 ↓
索引无法覆盖,因此仍然会回表/扫描。
✅ 优化方式:只选择必要字段
SELECT OrderId, Status, Amount, CreateTime
FROM Orders
WHERE Status = 1;
并可优化索引:
CREATE INDEX IX_Orders_Status
ON Orders(Status)
INCLUDE(OrderId, Amount, CreateTime);
五、避免 OR 导致扫描:改写为 UNION ALL
❌ 问题 SQL:OR 条件让优化器放弃索引
SELECT *
FROM Orders
WHERE Status = 1 OR Status = 2;
这种 OR 通常导致 Index Scan。
❌ 原有索引
CREATE INDEX IX_Orders_Status
ON Orders(Status);
尽管有索引,OR 会导致扫描。
✅ 优化方式:拆成两个查询 + UNION ALL
SELECT * FROM Orders WHERE Status = 1
UNION ALL
SELECT * FROM Orders WHERE Status = 2;
现在每个子查询都能使用 Index Seek。
六、JOIN 字段必须建立索引
❌ 问题示例:缺少 JOIN 字段索引 → Hash Join(昂贵)
SELECT o.OrderId, u.UserName
FROM Orders o
JOIN Users u ON o.UserId = u.Id;
如果:
- Orders.UserId 无索引
- Users.Id 无索引
那 SQL Server 只能使用 Hash Join。
❌ 没有索引
-- 无索引
✅ 解决:为 JOIN 字段增加索引
CREATE INDEX IX_Orders_UserId ON Orders(UserId);
CREATE INDEX IX_Users_Id ON Users(Id);
🚀 优化后
执行计划从 Hash Join 变为 Nested Loop,大幅降低 CPU 和内存使用。
七、利用统计信息提高计划质量
❌ 问题示例:统计信息陈旧导致优化器估算错误
SELECT *
FROM Orders
WHERE Amount > 1000;
如果数据大量新增/修改,优化器的估算行数(Estimated Rows)会完全错误,导致选择错误索引或错误 JOIN 类型。
❌ 表现
- Hash Join 出现
- 扫描出现
- Key Lookup 激增
✅ 解决方式:更新统计信息
UPDATE STATISTICS Orders WITH FULLSCAN;
或启用自动更新(推荐)
ALTER DATABASE db SET AUTO_UPDATE_STATISTICS ON;
八、处理参数嗅探:OPTION (RECOMPILE) 与本地变量技巧
❌ 问题示例:参数嗅探导致执行计划“偏向某个值”
例子:
CREATE PROCEDURE GetOrders
@UserId INT
AS
BEGIN
SELECT *
FROM Orders
WHERE UserId = @UserId;
END
如果首次执行传入一个极小结果集,会缓存“错误的轻量计划”,导致大用户的数据查询变慢。
解决方式一:强制重新生成执行计划
SELECT *
FROM Orders
WHERE UserId = @UserId
OPTION (RECOMPILE);
解决方式二:使用本地变量,让 SQL Server 不复用计划
DECLARE @uid INT = @UserId;
SELECT *
FROM Orders
WHERE UserId = @uid;
九、索引碎片影响性能:定期 Rebuild/Reorganize
❌ 问题示例:大量插入/删除导致索引碎片
SELECT *
FROM Orders
WHERE UserId = 20001;
即使有索引,也可能因碎片过高导致性能下降。
查看碎片率
SELECT index_id, avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('Orders'), NULL, NULL, NULL);
⭐ 优化方式
ALTER INDEX ALL ON Orders REBUILD; -- 碎片 > 30%
ALTER INDEX ALL ON Orders REORGANIZE; -- 10% ~ 30%
总结:查询优化的核心是“让 SQL Server 更容易做正确决定”
每一种优化方式最终都是为了一个目标:
让 SQL Server 的执行计划更准确、索引更高效,避免无意义的扫描和昂贵操作。
本篇文章给出的每一个优化都有:
- 慢 SQL 示例
- 产生慢的原因
- 对应索引和写法
- 优化后的高性能 SQL