为什么 SQL 服务器选择 "wrong" 索引?

Why is SQL Server choosing the "wrong" Index?

我有一个交易 table 有大约 2 亿条记录,一个主键聚集在 Id 和 2 个索引上:

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

Update STATISTICS dbo.[Transaction] IX_SiloId_ChangedTime_IncludeTime WITH FULLSCAN
Update STATISTICS dbo.[Transaction] IX_SiloId_Time_IncludeContent WITH FULLSCAN

这是我的查询:

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

现在发生的事情是出于任何原因 Sql 服务器选择 IX_SiloId_ChangedTime_IncludeTime。然后它需要永远。如果我使用 WITH (INDEX(IX_SiloId_Time_IncludeContent)) 我会立即得到结果。

正确的索引在这里很明显,但是SQL服务器选择了一个甚至没有按时索引的索引。

我无法理解这种行为,但根据我的阅读,最好避免对索引的提示,尽管我在考虑这个查询的情况下制作了这个索引。

所以问题是:我能做些什么来弄清楚为什么 SQL 服务器更喜欢“错误的”索引,即使存在更好的索引,我只是 运行 完整统计更新?

我已经创建了一个临时的 table 因为许多人建议 TVP 失败,但结果是一样的:

CREATE TABLE #Query
(
    SiloId bigint NOT NULL PRIMARY KEY CLUSTERED,
    Time datetime2(7) NOT NULL
)

执行计划:

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

https://www.brentozar.com/pastetheplan/?id=ByFshGAAP(这个是直播的,因为时间太长了)

指数:

CREATE NONCLUSTERED INDEX [IX_SiloId_Time_IncludeContent] ON [dbo].[Transaction]
(
    [SiloId] ASC,
    [Time] ASC
)
INCLUDE([SiloContent]) WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_SiloId_ChangedTime_IncludeTime] ON [dbo].[Transaction]
(
    [SiloId] ASC,
    [ChangedTime] ASC
)
INCLUDE([Time]) WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO

for whatever reason Sql Server choses IX_SiloId_ChangedTime_IncludeTime

执行计划不是这么说的。 SQL 未指定索引提示时,服务器选择 PK_Transaction 聚簇索引。

我很清楚为什么 SQL 服务器在查看执行计划时选择 PK_Transaction 而不是 IX_SiloId_Time_IncludeContent。原因是 基数估计不佳 。两个执行计划都显示 SQL 服务器估计连接操作产生 2.5182.000 行,但实际上产生了 4.155 行。如果 SQL 服务器选择 IX_SiloId_Time_IncludeContent 那么它估计需要执行 2.5182.000 次密钥查找。使用 IX_SiloId_Time_IncludeContent 索引进行 2.5182.000 次键查找,该计划比使用散列匹配和聚簇索引扫描的计划更昂贵。如果 SQL 服务器能够更好地估计,它会选择 IX_SiloId_Time_IncludeContent 因为只有 4.155 次密钥查找,该计划的成本要低得多。

那么,你能做什么?。我考虑两种选择:

  • 包括索引提示。索引提示的存在是有原因的。差的基数估计是包含提示的一个很好的理由。
  • 尝试使用第一个执行计划建议的覆盖索引。使用覆盖索引,不需要键查找。所以很有可能是SQL服务器选择了覆盖索引