如何调查 Sql Server 选择“错误”索引的原因?

Ily*_*dik 1 index sql-server execution-plan

我有一个包含大约 2 亿条记录的事务表,一个主键聚集在 Id 上,还有 2 个索引:

  • IX_SiloId_ChangedTime_IncludeTime
  • IX_SiloId_Time_IncludeContent

在继续实际查询以更新统计信息之前,我运行这两条语句

Update STATISTICS dbo.[Transaction] IX_SiloId_ChangedTime_IncludeTime WITH FULLSCAN
Update STATISTICS dbo.[Transaction] IX_SiloId_Time_IncludeContent WITH FULLSCAN
Run Code Online (Sandbox Code Playgroud)

这是我的查询:

DECLARE @Query SiloTimeQueryTableType -- (SiloId, Time) with primary key clustered on SiloId
INSERT INTO @Query VALUES 
(1, '2020-12-31'), -- 1000 total values, though it's still the same problem with just one

SELECT  t.*
FROM    [Transaction] t
INNER JOIN @Query q
    ON t.SiloId = q.SiloId
WHERE 
    t.Time >= q.Time

Run Code Online (Sandbox Code Playgroud)

现在发生的情况是 Sql Server 选择的原因IX_SiloId_ChangedTime_IncludeTime。然后就需要永远。如果我使用,WITH (INDEX(IX_SiloId_Time_IncludeContent))我会立即得到结果。

正确的索引在这里非常明显,但是 SQL Server 选择了一个甚至没有按时间建立索引的索引。

我无法理解这种行为,但从我读到的内容来看,最好避免索引的提示,尽管我在考虑这个查询的情况下创建了这个索引。

所以问题是:我能做些什么来尝试找出为什么 SQL Server 更喜欢“错误”的索引,即使存在更好的索引并且我只是运行完整的统计更新?

强制索引的查询计划(这里从临时表而不是 TVP 来检查这是否改变了答案所建议的任何内容,结果似乎是相同的):

在此输入图像描述

没有强制索引的查询计划:

在此输入图像描述

https://www.brentozar.com/pastetheplan/?id=rJOt3G00P

https://www.brentozar.com/pastetheplan/?id=ByFshGAAP(这个是实时的,因为它需要太长时间)

Jos*_*ell 6

这可能是由于表变量缺少列级统计信息。尝试相同的方法,但使用真正的临时表,如下所示:

CREATE TABLE #Query
(
    Id int NOT NULL,
    Time datetime NOT NULL
)

INSERT INTO #Query VALUES 
(1, '2020-12-31');

ALTER TABLE #Query
ADD CONSTRAINT PK_Query PRIMARY KEY (Id);

SELECT  t.*
FROM    [Transaction] t
INNER JOIN #Query q
    ON t.SiloId = q.SiloId
WHERE 
    t.Time >= q.Time
Run Code Online (Sandbox Code Playgroud)

然而,从实际提供的执行计划来看,问题肯定是由于错误的估计造成的。看一下这个:

在此输入图像描述

由于某种原因,优化器认为该连接将产生 25,000,000 行,而实际上只有 4,155 行。优化器不会选择执行 25,000,000 次键查找(因为索引不包含所有列),因此您最终会扫描聚集索引。这在各种博客等上被称为“临界点”。

“最简单”的选项是进行索引覆盖(包括表中的所有列),或者仅选择索引中的列。这样就避免了键查找,你自然应该得到这个计划。但是,这可能不切实际(不确定表中其他列的数据类型是什么,等等)。

更好的选择是以某种方式修正估计。您可以尝试向临时表添加索引Time,以防提供更好的统计信息(加载表后):

CREATE NONCLUSTERED INDEX IX_Time
ON #Query (Time);
Run Code Online (Sandbox Code Playgroud)

您还可以尝试以不同的形式重写查询,看看是否得到不同的结果/更好的估计,如下所示:

SELECT  t.*
FROM    [Transaction] t
WHERE EXISTS
(
    SELECT null
    FROM #Query q
    WHERE 
        q.SiloId = t.SiloId 
        AND t.Time >= q.Time
)
Run Code Online (Sandbox Code Playgroud)

或者通过在查询末尾添加以下提示来查看旧基数估计器如何处理此查询:

OPTION (USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'))
Run Code Online (Sandbox Code Playgroud)

所以问题是:我能做些什么来尝试找出为什么 SQL Server 更喜欢“错误”的索引,即使存在更好的索引并且我只是运行完整的统计更新?

一般来说,使用索引提示来“强制”您想要的索引,然后比较该计划与自然计划之间的成本或估计差异是有用的。这可以提供有关为什么未选择所需索引的线索。

FORCE_LEGACY_CARDINALITY_ESTIMATION 尽管立即选择了正确的索引!虽然不知道这意味着什么...新的有一些错误吗?

我不会说这是一个错误 - 他们只是有非常不同的方法来产生估计。与新 CE 相比,旧版 CE 在某些情况下仍能更好地工作。

如果您想深入了解一个特定差异(这也提供了一种查看统计数据以及如何生成估计值的方法),请查看 Paul White 的博客文章:使用直方图粗对齐进行 SQL Server 连接估计