一、执行计划是什么?为什么需要看它?
当你写了一条SQL语句交给数据库执行时,数据库并不是直接照着你写的语句去操作数据,而是会先制定一个"行动计划",这就是执行计划。就像你要去旅行,得先规划路线一样。
执行计划会告诉你:
- 数据库准备怎么查找数据
- 先用哪个索引
- 要不要排序
- 预计要处理多少数据
看执行计划最大的好处就是能发现SQL语句的"毛病"。比如你本以为会走索引的查询,实际上却全表扫描了。
-- 技术栈:SQL Server 2019
-- 示例1:查看简单查询的执行计划
SELECT * FROM Orders WHERE CustomerID = 'ALFKI';
-- 实际执行时,可以这样查看计划
SET SHOWPLAN_TEXT ON;
GO
SELECT * FROM Orders WHERE CustomerID = 'ALFKI';
GO
SET SHOWPLAN_TEXT OFF;
二、执行计划里有哪些关键信息要看
执行计划看起来复杂,其实主要关注几个关键点就够了:
- 操作类型:是扫描(Scan)还是查找(Seek)?扫描通常比查找慢
- 预估行数:数据库估计要处理多少行数据
- 实际行数:实际处理了多少行数据
- 开销占比:每个步骤占整个查询的消耗比例
-- 技术栈:SQL Server 2019
-- 示例2:分析执行计划中的关键指标
-- 先创建一个测试表
CREATE TABLE Products (
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(100),
Price DECIMAL(10,2),
CategoryID INT
);
-- 插入测试数据
INSERT INTO Products VALUES
(1, '苹果', 5.5, 1),
(2, '香蕉', 3.2, 1),
(3, '电视', 2999, 2),
(4, '手机', 3999, 2);
-- 查看执行计划
SELECT * FROM Products WHERE CategoryID = 1;
在这个例子中,你会看到执行计划显示是表扫描还是索引查找,预估处理多少行等信息。
三、常见的性能问题及优化方法
3.1 全表扫描问题
当看到执行计划中出现"Table Scan"时,通常意味着性能问题。就像在图书馆找书,不是通过目录而是从第一本开始翻。
解决方法:
- 为查询条件创建合适的索引
- 检查WHERE条件是否真的能用上索引
-- 技术栈:SQL Server 2019
-- 示例3:解决全表扫描问题
-- 先看看没有索引的情况
SELECT * FROM Products WHERE CategoryID = 1;
-- 添加索引后
CREATE INDEX IX_Products_Category ON Products(CategoryID);
-- 再次查看执行计划
SELECT * FROM Products WHERE CategoryID = 1;
3.2 键查找问题
有时候执行计划会出现"Key Lookup",这意味着数据库先通过索引找到了主键,但还要回表查其他字段。
解决方法:
- 考虑创建覆盖索引
- 只查询必要的字段
-- 技术栈:SQL Server 2019
-- 示例4:解决键查找问题
-- 原始查询
SELECT ProductName, Price FROM Products WHERE CategoryID = 1;
-- 优化方案1:创建覆盖索引
CREATE INDEX IX_Products_Category_Cover ON Products(CategoryID) INCLUDE (ProductName, Price);
-- 优化方案2:只查询索引列
SELECT CategoryID FROM Products WHERE CategoryID = 1;
四、高级优化技巧
4.1 参数嗅探问题
SQL Server会对第一次执行的参数值"记住"并重用执行计划,这可能导致参数变化时性能下降。
解决方法:
- 使用OPTION(RECOMPILE)
- 使用本地变量
-- 技术栈:SQL Server 2019
-- 示例5:解决参数嗅探问题
CREATE PROCEDURE GetProductsByCategory
@CategoryID INT
AS
BEGIN
-- 方法1:使用OPTION(RECOMPILE)
SELECT * FROM Products WHERE CategoryID = @CategoryID
OPTION (RECOMPILE);
-- 方法2:使用本地变量
DECLARE @LocalCategoryID INT = @CategoryID;
SELECT * FROM Products WHERE CategoryID = @LocalCategoryID;
END
4.2 统计信息更新
SQL Server依靠统计信息来生成执行计划,如果统计信息过时,计划可能不准确。
解决方法:
- 定期更新统计信息
- 在查询前手动更新
-- 技术栈:SQL Server 2019
-- 示例6:更新统计信息
-- 更新单个表的统计信息
UPDATE STATISTICS Products;
-- 更新整个数据库的统计信息
EXEC sp_updatestats;
五、实际案例分析
让我们看一个真实场景中的优化案例:
-- 技术栈:SQL Server 2019
-- 示例7:实际优化案例
-- 假设有一个订单表
CREATE TABLE Orders (
OrderID INT PRIMARY KEY,
CustomerID NVARCHAR(10),
OrderDate DATETIME,
TotalAmount DECIMAL(12,2)
);
-- 插入大量测试数据(这里简化)
INSERT INTO Orders VALUES
(1, 'ALFKI', '2023-01-01', 100),
(2, 'ALFKI', '2023-01-02', 200),
-- ...假设还有很多数据
(10000, 'BOTTM', '2023-12-31', 300);
-- 问题查询:查找某客户某时间段的订单
SELECT * FROM Orders
WHERE CustomerID = 'ALFKI'
AND OrderDate BETWEEN '2023-01-01' AND '2023-01-31';
-- 执行计划分析后发现:
-- 1. 使用了CustomerID上的索引,但OrderDate条件没用到
-- 2. 返回了大量不需要的字段
-- 优化方案:
-- 1. 创建复合索引
CREATE INDEX IX_Orders_Customer_Date ON Orders(CustomerID, OrderDate);
-- 2. 只查询必要字段
SELECT OrderID, OrderDate, TotalAmount
FROM Orders
WHERE CustomerID = 'ALFKI'
AND OrderDate BETWEEN '2023-01-01' AND '2023-01-31';
六、总结与最佳实践
经过上面的讲解,我们可以总结出一些SQL Server查询优化的最佳实践:
- 养成看执行计划的习惯:不要凭感觉优化,要看实际执行计划
- 关注关键指标:特别是扫描操作、预估/实际行数差异
- 索引不是越多越好:每个索引都会增加写入开销
- 定期维护统计信息:过时的统计信息会导致糟糕的执行计划
- 考虑查询重写:有时候换个写法性能会大不相同
最后记住,优化是一个持续的过程。随着数据量的变化,今天优化的查询明天可能又需要重新审视。
评论