SQL Server 如何选择要使用的索引

Tho*_* D. 7 index sql-server optimization

我最近开始致力于索引我们数据库中的各种视图。通常,其中一些表仅用于连接,很少用于 where 或 order by 语句。这意味着我们正在创建多个索引(或一个较大的相对效率较低的索引),它们都以主键为键。我应该澄清一下,通过包含此表中的任何内容,没有位置或顺序,并且在我遇到问题的视图中没有键查找。

如果 SQL 有多个索引都以 tbl.ID 为键,它如何选择使用一个索引而不是另一个?我发现即使从索引中提取的数据相同,它也会因视图而异。一般来说,选择一个索引而不是另一个索引在效率上没有巨大的收益/损失;让我相信它选择了一个“足够好”的索引并继续前进(因为有很多键都来自同一列)。

此外,由于这个原因,是否应该避免使用相同的键创建多个索引?

我目前正在处理的具体问题是选择这样的索引:

CREATE NONCLUSTERED INDEX [index1]
ON a.tbl ([ID])
INCLUDE (number,name,year, ...)
Run Code Online (Sandbox Code Playgroud)

包含中有 14 列的地方(不幸的是,但根据我们当前的查询结构是必需的)

在更有效的索引上:

CREATE NONCLUSTERED INDEX [index2]
ON a.tbl ([ID])
INCLUDE (number,name,year, ...)
Run Code Online (Sandbox Code Playgroud)

包含中只有 5 列的地方。

两个索引都有视图需要的信息,但一个明显更小,使用效率更高。需要存在更大的索引来覆盖调用其中所有列(大约表的一半)的大量视图。

可能值得一提的是,我可以选择像上面那样创建一个海量索引,它几乎覆盖了所有视图,但效率略低于这些较小的索引。从我的测试来看,添加这些额外的索引似乎不会增加更新表的显着成本;所以我选择创建它们以获得小的性能提升。通常,它们会在需要时使用,但在某些情况下,优化器会无缘无故地选择效率较低的选项。

Aar*_*and 9

在我有限的实验中:

  • 当查询必须执行扫描时,SQL Server 每次都会执行查找最窄索引的工作。
  • 当期望查询执行查找时,它不关心索引宽度,它只使用创建的最后一个覆盖索引

这种是有道理的。如果您要在这两种情况下添加搜索完美索引的规则,那么您应该在最节省时间的时候进行。在寻找更好的索引时花费的精力在所有行而不是每行的节省方面并不重要时,不太可能值得。“后进”可能是有意为之,假设您创建的最后一个覆盖索引可能是您创建的最好的索引,或者它可能只是随意和巧合(例如缺少索引推荐中的列顺序)。

证明(嗯,某种证明)

我的测试设置非常简单:

CREATE TABLE dbo.t1
(
  id int IDENTITY(1,1) PRIMARY KEY,
  sn1 sysname, tn1 sysname, cn1 sysname, typ1 sysname,
  sn2 sysname, tn2 sysname, cn2 sysname, typ2 sysname,
  sn3 sysname, tn3 sysname, cn3 sysname, typ3 sysname,
  sn4 sysname, tn4 sysname, cn4 sysname, typ4 sysname
);
GO
SET NOCOUNT ON;
GO
INSERT dbo.t1
(
  sn1,tn1,cn1,typ1,sn2,tn2,cn2,typ2,
  sn3,tn3,cn3,typ3,sn4,tn4,cn4,typ4
)
SELECT s.name, t.name, c.name, typ.name,
       s.name, t.name, c.name, typ.name,
       s.name, t.name, c.name, typ.name,
       s.name, t.name, c.name, typ.name
FROM sys.schemas AS s
CROSS JOIN sys.objects AS t
INNER JOIN sys.all_columns AS c
ON t.[object_id] = c.[object_id]
INNER JOIN sys.types AS typ
ON c.user_type_id = typ.user_type_id;
GO

CREATE VIEW dbo.v1
AS
  SELECT id,sn1,tn1
  FROM dbo.t1;
GO
Run Code Online (Sandbox Code Playgroud)

这将 13,260 行放入我的表中(您的结果会有所不同)。然后我以不同的顺序反复创建了相同的三个索引:

-- widest first
CREATE INDEX ix_wide ON dbo.t1(sn1) 
  INCLUDE(tn1,cn1,typ1,sn2,tn2,cn2,typ2,sn3,tn3,cn3,typ3,sn4,tn4,cn4,typ4);
GO
CREATE INDEX ix_mid ON dbo.t1(sn1) INCLUDE(tn1,cn1,sn2,tn2,cn2,typ2,sn3,tn3);
GO
CREATE INDEX ix_small ON dbo.t1(sn1) INCLUDE(tn1,cn1);
GO

-- widest last = ix_small then ix_mid then ix_wide
-- middle1 = ix_mid then ix_wide then ix_small
-- middle2 = ix_small then ix_wide then ix_mid
Run Code Online (Sandbox Code Playgroud)

然后在这四个案例中的每一个中,我运行了这两个查询并调查了计划:

DBCC FREEPROCCACHE;
GO
SELECT id,sn1,tn1 FROM dbo.v1; -- scan
GO
SELECT id,sn1,tn1 FROM dbo.v1 WHERE sn1 LIKE N'q%'; -- seek
Run Code Online (Sandbox Code Playgroud)

结果:

         widest first  widest last  middle1   middle2
-------  ------------  -----------  --------  --------
   scan    ix_small      ix_small   ix_small  ix_small
   seek    ix_small      ix_wide    ix_small  ix_mid
Run Code Online (Sandbox Code Playgroud)

扫描总是选择最窄的索引。搜索总是选择最后创建的索引(因为全部覆盖)。我没有扩展测试以在混合中包含一个非覆盖索引,因为我不怀疑它会改变这个结果。

道德(嗯,有点道德)

这里有两个道德:

  1. 如果您希望在搜索中尽可能使用最窄的索引,请先创建最宽的索引。
  2. 这种优化可能是值得的一小部分用例。例如,搜索返回很多行并且较宽的覆盖索引具有非常大的列,这些列不在较窄的索引中。在这些情况下,可能值得用提示(以及所有涉及的注意事项)明确指定索引,而不是依赖于我在此处观察到的行为。

可能我在这里遗漏了一些内部原理和启发式方法,Paul 将对此进行澄清。我还尝试启用跟踪标志 302 来输出索引选择信息,但这似乎不适用于现代版本的 SQL Server。