SQL 未使用服务器非聚集索引

SQL Server non-clustered index is not being used

一个应用程序(其底层代码无法修改)正在运行针对相对较大的[=61]创建一个select语句=](~180 万行,2GB)并在数据库服务器上造成巨大的性能瓶颈。

table本身有大约。 120 列不同的数据类型。 select 语句 select 根据 2 列的值对这些列中的大约 100 列进行了索引,这些列都已单独索引并一起索引。

例如

SELECT  
    column1, 
    column2,
    column3,
    column4,
    column5, 
    and so on.....
FROM
    ITINDETAIL 
WHERE 
    (column23 = 1 AND column96 = 1463522) 

但是,SQL 服务器选择忽略索引,而是扫描永远需要的聚簇 (PK) 索引(这在 42 秒后被取消,众所周知,在繁忙的生产中需要 8 分钟以上的时间数据库)。

如果我简单地更改 select 语句,仅用 select count(*) 替换所有列,则会使用索引并在毫秒内产生 return。

编辑:我相信ITINDETAIL_004(column23 和 column96 一起索引)是应该用于原始语句的索引。

令人困惑的是,如果我在 table 上为 where 子句中的两列创建非聚集索引,如下所示:

CREATE NONCLUSTERED INDEX [ITINDETAIL20220504] 
ON [ITINDETAIL] ([column23] ASC, [column96] ASC)
INCLUDE (column1, column2, column3, column4, column5,  
         and so on..... )

并在索引中包含 ALL 语句中 select 的其他列(在我看来,这实际上只是创建一个 TABLE!),然后 运行 原来的 select 语句,使用新的 HUGE 索引,结果很快 returned:

但是,我不确定这是解决问题的正确方法。

如何强制 SQL 服务器使用我认为正确的索引?

版本是:SQL Server 2017

根据评论中的其他详细信息,您希望 SQL 服务器使用的索引似乎不是覆盖索引;这意味着索引不包含查询中引用的所有列。因此,如果 SQL 服务器要使用所述索引,则它需要首先对索引进行查找,然后对聚集索引执行键查找以获取该行的完整详细信息。这样的查找可能很昂贵。

由于您想要的索引未被覆盖,SQL 服务器已确定您希望它使用的索引会产生一个较差的查询计划,无法简单地扫描整个聚集索引;根据定义,它涵盖 INCLUDE 不在 CLUSTERED INDEX.

中的所有其他列

对于您的索引 ITINDETAIL20220504,您有 INCLUDEd SELECT 中的所有列,这意味着它正在覆盖。这意味着 SQL 服务器可以对索引执行查找,并从该查找中获取所需的所有信息;这比先查找后进行键查找的成本要低得多,而且比扫描整个聚集索引要快。这就是此信息有效的原因。


我们可以使用充满书籍的图书馆类型场景将其放入某种类比中,以帮助更多地解释这个想法:

假设聚簇索引是图书馆中 每本 图书的列表,按其 ISBN 编号(主键)排序。除了 ISBN 编号,您还有作者、标题、出版日期、出版商的详细信息,如果是精装书或平装书,书脊的颜色,图书所在图书馆的部分,书柜和书架.

现在假设您想要获取作者 Brandon Sanderson 在 2015 年 1 月 1 日或之后出版的任何书籍。如果您随后想要 可以 浏览整个列表,一本又一本,找到该作者的书,检查出版日期,然后记下它的位置,这样您就可以去访问每个位置并收集这本书。这实际上是聚集索引扫描。

现在假设您再次拥有图书馆所有图书的清单。该列表包含作者、出版日期和 ISBN(主键),并按作者和出版日期排序。你想完成同样的任务;获取作者 Brandon Sanderson 在 2015 年 1 月 1 日或之后出版的任何书籍。现在您可以轻松浏览该列表并找到所有这些书,但您不知道它们在哪里。因此,即使您直接进入列表的 Brandon Sanderson“部分”,您仍然需要记下所有 ISBN,然后在原始列表中找到每个 ISBN,获取它们的位置和标题。这是你的索引ITINDETAIL_004;您可以轻松找到要筛选的行,但您没有所有信息,因此之后您必须去其他地方。

最后我们有第三个列表,这个列表按作者和出版日期排序(和第二个列表一样),还包括书名、图书所在图书馆的部分、书架和书架,以及 ISBN(主键)。此列表非常适合您的任务;它的顺序正确,因为您可以轻松转到 Brandon Sanderson,然后转到 2015 年 1 月 1 日或之后出版的第一本书, 它具有书名和位置。 这个就是你的INDEXITINDETAIL20220504;它具有您想要的顺序的信息,并包含 所有 您要求的信息。


综上所述,您 可以 强制 SQL 服务器选择索引,但正如我在 comment 中所说:

In truth, rarely is the index you think is the correct is the correct index if SQL Server thinks otherwise. Despite what some believe, SQL Server is very good at making informed choices about what index(es) it should be using, provided your statistics are up to date, and the cached plan isn't based on old and out of date information.

但是,让我们演示一下如果您使用简单的设置会发生什么:

CREATE TABLE dbo.SomeTable (ID int IDENTITY(1,1) PRIMARY KEY,
                            SomeGuid uniqueidentifier DEFAULT NEWID(),
                            SomeDate date)
GO
INSERT INTO dbo.SomeTable (SomeDate)
SELECT DATEADD(DAY,T.I,'19000101')
FROM dbo.Tally(100000,0) T;

现在我们有一个包含 100001 行的 table,主键在 ID 上。现在让我们做一个查询,它是你的一个过于简化的版本:

SELECT ID,
       SomeGuid
FROM dbo.SomeTable
WHERE SomeDate > '20220504';

毫不奇怪,这会导致聚集索引扫描:

好的,让我们再次在 SomeDate 和 运行 查询上添加索引:

CREATE INDEX IX_NonCoveringIndex ON dbo.SomeTable (SomeDate);
GO
--Index Scan of Clustered index
SELECT ID,
       SomeGuid
FROM dbo.SomeTable
WHERE SomeDate > '20220504';

相同的结果,SSMS 甚至建议了一个索引:

现在,正如我提到的,您可以 强制SQL 服务器使用特定索引。让我们这样做,看看会发生什么:

SELECT ID,
       SomeGuid
FROM dbo.SomeTable WITH (INDEX(IX_NonCoveringIndex))
WHERE SomeDate > '20220504';

这正是我建议的计划;一键查找:

这是昂贵的。事实上,如果我们打开 IO 和 Time 的统计,没有索引提示的查询用了 40 毫秒,有提示的查询在第一个 运行 中用了 107 毫秒。随后的 运行 都进行了第二个查询,时间大约是第一个查询的 两倍 。 IO 明智的第一个查询有一个简单的扫描和 398 个逻辑读取;后者有 5 次扫描和 114403 次逻辑读取!

现在,最后,让我们添加覆盖索引和 运行:

CREATE INDEX IX_CoveringIndex ON dbo.SomeTable (SomeDate) INCLUDE (SomeGuid);
GO

SELECT ID,
       SomeGuid
FROM dbo.SomeTable
WHERE SomeDate > '20220504';

在这里我们可以看到我们想要的那个寻求:

如果我们再次查看 IO 和时间,与之前的 2 次相比,我们得到 1 次扫描,202 次逻辑读取,并且在大约 25 毫秒内 运行ning。