内联变量时,为什么SQL Server使用更好的执行计划?


32

我有一个要优化的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

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)

当我完全按照上面的描述执行查询时,SQL Server将扫描第一个索引,从而导致189,703逻辑读取和2-3秒的持续时间。

当我内联@Id变量并再次执行查询时,SQL Server将查找第二个索引,从而导致仅104次逻辑读取和0.001秒的持续时间(基本上是即时的)。

我需要变量,但是我希望SQL使用好的计划。作为一个临时解决方案,我在查询上添加了索引提示,查询基本上是即时的。但是,我尽量避免使用索引提示。我通常假设如果查询优化器无法完成其工作,那么我可以做一些事情(或停止做些事情)来帮助它,而无需明确告诉它该做什么。

那么,当我内联变量时,为什么SQL Server会提出一个更好的计划?

Answers:


44

在SQL Server中,有三种常见的非联接谓词形式:

具有文字值:

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

参数

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

使用局部变量

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

结果

当您使用文字值,并且您的计划不是a)琐碎的和b)简单参数化的或c)您没有启用强制参数化时,优化器会为该值创建一个非常特殊的计划。

使用参数时,优化器将为该参数创建一个计划(这称为参数嗅探),然后重用该计划,缺少重新编译提示,计划缓存逐出等。

当你使用一个局部变量,优化使得对...计划的东西

如果要运行此查询:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

该计划将如下所示:

坚果

该局部变量的估计行数如下所示:

坚果

即使查询返回的计数为4,744,427。

未知的局部变量不会将直方图的“好”部分用于基数估计。他们使用基于密度矢量的猜测。

坚果

SELECT 5.280389E-05 * 7250739 AS [poo]

这会给您382.86722457471,这是优化器所做的猜测。

这些未知的猜测通常是非常糟糕的猜测,并且经常会导致错误的计划和错误的索引选择。

修好吗?

您的选择通常是:

  • 脆性指数提示
  • 潜在的昂贵的重新编译提示
  • 参数化动态SQL
  • 存储过程
  • 改善当前指标

您的选择具体是:

改进当前索引意味着将其扩展为覆盖查询所需的所有列:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

假设Id值具有合理的选择性,这将为您提供良好的计划,并通过为优化器提供“显而易见的”数据访问方法来帮助优化器。

更多阅读

您可以在此处阅读有关参数嵌入的更多信息:


12

我将假设您的数据有偏差,您不想使用查询提示来强制优化器执行该操作,并且您需要为的所有可能输入值获得良好的性能@Id。如果您愿意创建以下一对索引(或它们的等效索引),则可以确保查询计划对任何可能的输入值仅需要少量的逻辑读取:

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

以下是我的测试数据。我在表中放入了1300万行,并使其中一半的值'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;

首先,此查询可能看起来有些奇怪:

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;

它旨在利用索引的顺序来进行几次逻辑读取,以找到最小值或最大值。的CROSS JOIN是有没有得到正确的结果时,有没有为任何匹配行@Id价值。即使我过滤表中最流行的值(匹配650万行),我也只会得到8个逻辑读取:

表“ MyTable”。扫描计数2,逻辑读取8

这是查询计划:

在此处输入图片说明

两个索引都查找0或1行。这非常高效,但是创建两个索引可能对您的情况来说是过大的。您可以考虑使用以下索引:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

现在,原始查询的查询计划(带有可选MAXDOP 1提示)看起来有些不同:

在此处输入图片说明

不再需要键查找。有了一个更好的访问路径,该路径应该适用于所有输入,因此您不必担心优化器由于密度矢量而选择了错误的查询计划。但是,如果您寻求一个流行的@Id价值,那么该查询和索引将不会像其他查询和索引那样高效。

表“ MyTable”。扫描计数1,逻辑读取33757


2

在这里我无法回答为什么,但是确保查询以您想要的方式运行的快捷方法是:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

这可能会导致表或索引将来可能更改的风险,从而使此优化功能失灵,但是如果需要,它可以使用。希望有人可以按照您的要求为您提供根本原因的答案,而不是这种解决方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.