Rai*_*olt 33 performance sql-server execution-plan
我有一个要优化的 SQL 查询:
DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT
Id,
MIN(SomeTimestamp),
MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
AND SomeBit = 1
GROUP BY Id
Run Code Online (Sandbox Code Playgroud)
MyTable 有两个索引:
CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)
CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)
Run Code Online (Sandbox Code Playgroud)
当我完全按照上面写的方式执行查询时,SQL Server 扫描第一个索引,导致 189,703 次逻辑读取和 2-3 秒的持续时间。
当我内联@Id变量并再次执行查询时,SQL Server 寻找第二个索引,导致只有 104 次逻辑读取和 0.001 秒的持续时间(基本上是即时的)。
我需要变量,但我希望 SQL 使用好的计划。作为临时解决方案,我在查询上放置了索引提示,查询基本上是即时的。但是,我尽量避免使用索引提示。我通常假设如果查询优化器无法完成它的工作,那么我可以做(或停止做)一些事情来帮助它,而无需明确告诉它该做什么。
那么,当我内联变量时,为什么 SQL Server 会提出更好的计划?
Eri*_*ing 47
在 SQL Server 中,有三种常见的非连接谓词形式:
SELECT COUNT(*) AS records
FROM dbo.Users AS u
WHERE u.Reputation = 1;
Run Code Online (Sandbox Code Playgroud)
CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
SELECT COUNT(*) AS records
FROM dbo.Users AS u
WHERE u.Reputation = @Reputation;
END;
Run Code Online (Sandbox Code Playgroud)
DECLARE @Reputation INT = 1
SELECT COUNT(*) AS records
FROM dbo.Users AS u
WHERE u.Reputation = @Reputation;
Run Code Online (Sandbox Code Playgroud)
当您使用文字值,并且您的计划不是 a)平凡和 b) 简单参数化或 c) 您没有打开强制参数化时,优化器会为该值创建一个非常特殊的计划。
当您使用参数时,优化器将为该参数创建一个计划(这称为参数嗅探),然后重用该计划,没有重新编译提示,计划缓存逐出等。
当您使用局部变量时,优化器会为……某事制定计划。
如果您要运行此查询:
DECLARE @Reputation INT = 1
SELECT COUNT(*) AS records
FROM dbo.Users AS u
WHERE u.Reputation = @Reputation;
Run Code Online (Sandbox Code Playgroud)
该计划将如下所示:
该局部变量的估计行数如下所示:
即使查询返回 4,744,427 的计数。
未知的局部变量不使用直方图的“好”部分进行基数估计。他们使用基于密度向量的猜测。
SELECT 5.280389E-05 * 7250739 AS [poo]
这会给你382.86722457471,这是优化器所做的猜测。
这些未知的猜测通常是非常糟糕的猜测,通常会导致糟糕的计划和糟糕的索引选择。
您的选择通常是:
您的选择特别是:
改进当前索引意味着扩展它以涵盖查询所需的所有列:
CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);
Run Code Online (Sandbox Code Playgroud)
假设Id值具有合理的选择性,这将为您提供一个很好的计划,并通过为其提供“明显”的数据访问方法来帮助优化器。
您可以在此处阅读有关参数嵌入的更多信息:
Joe*_*ish 13
我将假设您有倾斜的数据,您不想使用查询提示来强制优化器做什么,并且您需要为所有可能的输入值获得良好的性能@Id。如果您愿意创建以下一对索引(或它们的等效项),您可以获得保证只需要少量逻辑读取任何可能的输入值的查询计划:
CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;
Run Code Online (Sandbox Code Playgroud)
下面是我的测试数据。我将 13 M 行放入表中,并使其中一半'3A35EA17-CE7E-4637-8319-4C517B6E48CA'的Id列值为。
DROP TABLE IF EXISTS dbo.MyTable;
CREATE TABLE dbo.MyTable (
Id uniqueidentifier,
SomeTimestamp DATETIME2,
SomeInt INT,
SomeBit BIT,
FILLER VARCHAR(100)
);
INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Run Code Online (Sandbox Code Playgroud)
这个查询起初可能看起来有点奇怪:
DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'
SELECT
@Id,
st.SomeTimestamp,
si.SomeInt
FROM (
SELECT TOP (1) SomeInt, Id
FROM dbo.MyTable
WHERE Id = @Id
AND SomeBit = 1
ORDER BY SomeInt DESC
) si
CROSS JOIN (
SELECT TOP (1) SomeTimestamp, Id
FROM dbo.MyTable
WHERE Id = @Id
AND SomeBit = 1
ORDER BY SomeTimestamp ASC
) st;
Run Code Online (Sandbox Code Playgroud)
它旨在利用索引的顺序通过一些逻辑读取来查找最小值或最大值。的CROSS JOIN是有没有得到正确的结果时,有没有为任何匹配行@Id价值。即使我过滤表中最流行的值(匹配 650 万行),我也只能得到 8 次逻辑读取:
表“我的表”。扫描计数 2,逻辑读取 8
这是查询计划:
两个索引都查找 0 或 1 行。它非常有效,但对于您的场景来说,创建两个索引可能有点过头了。您可以考虑使用以下索引:
CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;
Run Code Online (Sandbox Code Playgroud)
现在原始查询的查询计划(带有可选MAXDOP 1提示)看起来有点不同:
不再需要键查找。有了更好的访问路径,它应该适用于所有输入,您不必担心优化器由于密度向量而选择错误的查询计划。但是,如果您寻找流行的@Id值,则此查询和索引的效率将不如另一个。
表“我的表”。扫描计数 1,逻辑读取 33757