为什么我在查询计划(SQL 服务器)中使用 Key Lookup 进行排序操作

Why do I have Sort operation with Key Lookup in query plan (SQL Server)

我们有一个应用程序执行一些我们无法更改的查询,就像这样(我使用 Whosebug2013 数据库来演示它):

SELECT *
FROM dbo.Posts p
WHERE CHARINDEX(N'Aptana', p.Title) > 0

我们的数据库有类似的结构——行很宽,由很多不同的列组成,包括 nvarchar(smth) 和 nvarchar(max) 数据类型。

这个查询有这个查询计划(如我们的,聚簇索引扫描),显然:

  |--Clustered Index Scan(OBJECT:([Whosebug2013].[dbo].[Posts].[PK_Posts_Id] AS [p]), WHERE:(charindex(N'Aptana',[Whosebug2013].[dbo].[Posts].[Title] as [p].[Title])>(0))) 

actual execution plan with clustered index scan

我们在这个专栏上有一个索引,我在dbo.Posts(标题)上创建了一个:

CREATE INDEX myPleasureSort ON dbo.Posts (Title);

我无法更改查询,但我可以创建索引并使用计划指南添加 INDEX HINT。 我不得不说,我们的用户总是使用这种查询来查找几行,可能是 5000 万中的 100 行,因此非聚集索引扫描应该更快,资源占用更少。

所以当我尝试这个时:

SELECT *
FROM dbo.Posts p
WHERE CHARINDEX(N'Aptana', p.Title) > 0
OPTION (MAXDOP 1, TABLE HINT(p, INDEX (myPleasureSort)))

结果是:

|--Nested Loops(Inner Join, OUTER REFERENCES:([p].[Id], [Expr1002]) WITH UNORDERED PREFETCH)
   |--Sort(ORDER BY:([p].[Id] ASC))
   |    |--Index Scan(OBJECT:([Whosebug2013].[dbo].[Posts].[myPleasureSort] AS [p]),  WHERE:(charindex(N'Aptana',[Whosebug2013].[dbo].[Posts].[Title] as [p].[Title])>(0)))
   |--Clustered Index Seek(OBJECT:([Whosebug2013].[dbo].[Posts].[PK_Posts_Id] AS [p]), SEEK:([p].[Id]=[Whosebug2013].[dbo].[Posts].[Id] as [p].[Id]) LOOKUP ORDERED FORWARD)

actual execution plan with key lookup and sort

这是我的问题。为什么我在Key Lookup之前有这个排序操作?我想正因为如此,我获得了巨大的内存授权,我不想在生产中使用它。

The query memory grant detected "ExcessiveGrant", which may impact the reliability. Grant size: Initial 566496 KB, Final 566496 KB, Used 216 KB.

我找到了这个索引的解决方法:

CREATE INDEX myPleasure ON dbo.Posts (Id, Title);

对于这个查询,我有下一个查询计划:

SELECT *
FROM dbo.Posts p
WHERE CHARINDEX(N'Aptana', p.Title) > 0
OPTION (MAXDOP 1, TABLE HINT(p, INDEX (myPleasure)))

  |--Nested Loops(Inner Join, OUTER REFERENCES:([p].[Id], [Expr1002]) WITH UNORDERED PREFETCH)
       |--Index Scan(OBJECT:([Whosebug2013].[dbo].[Posts].[myPleasure] AS [p]),  WHERE:(charindex(N'Aptana',[Whosebug2013].[dbo].[Posts].[Title] as [p].[Title])>(0)) ORDERED FORWARD)
       |--Clustered Index Seek(OBJECT:([Whosebug2013].[dbo].[Posts].[PK_Posts_Id] AS [p]), SEEK:([p].[Id]=[Whosebug2013].[dbo].[Posts].[Id] as [p].[Id]) LOOKUP ORDERED FORWARD)

actual execution plan with key lookup without sort

但我更愿意只在 nvarchar 列上使用索引,以便有可能将它与类似 'str%'.

的内容一起使用

提前谢谢你,请原谅我糟糕的英语。

更新:SELECT @@版本:

Microsoft SQL Server 2017 (RTM-CU20) (KB4541283) - 14.0.3294.2 (X64)

更新 2:感谢@MattM,它看起来像我的情况:

SQL 总的来说,服务器确实将您的最大利益放在心上。如果它正在执行聚簇索引扫描或使用排序运算符,可能是因为查询优化器在必须搜索计划时找到了它可能找到的最佳计划。

内存授予是SQL服务器确定查询需要完成的——这包括读取所有数据所需的内存。过多的内存授予意味着 SQL 服务器使用的内存 远远少于 优化器认为查询所需的内存。

查询优化器根据 可能 要 return 编辑的数据的大小来估计此内存量。

(优化器会在您的查询计划中考虑 Sort 运算符。但我怀疑这不是这里的问题。)

我的猜测是您的 SELECT * 语句中的许多列是大对象 -- VARCHAR and/or NVARCHAR 数据类型的 100+ 或 MAX 数据长度 -- 不使用它们的全部长度。

例如,假设 [Title] 列是一个 NVARHCAR(255),并且该列中的大多数值的长度都小于 50 个字符。

查询优化器看不到该列中的数据。只是数据类型。它必须假定该列中的任何或所有数据,对于它期望 return 的所有行,可能是完整的 255 个字符长。

因此,它将请求足够的内存来容纳此列。 (它可能不会得到它所要求的一切,因为 SQL 服务器不是一个彻头彻尾的白痴。SQL 服务器将分配一个修改后的内存授权,但它仍然是相当大的。)

当发现 [Title] 列中的大多数数据长度小于 50 个字符时,SQL 服务器将抱怨它必须清除所有内存只是为了这个愚蠢的事实查询,无论如何都没有使用它!

将任何这些大对象列的数据类型调整为更窄的长度将意味着查询优化器请求更少的内存。

但是,在您的情况下,SELECT * 和非 SARGable WHERE 子句可能仍会使查询 运行 次优。

真的,提高查询性能的唯一方法是重写它,使其成为 SARGable。

例如

SELECT  *
FROM    dbo.Posts AS p
WHERE   p.Title LIKE '%Aptana%' ;

这甚至可以在不需要查询提示的情况下使用您的索引。