按连接表的列排序时提高性能

Ale*_*eks 7 sql-server sql-server-2014 query-performance

我有一个包含查找表的外键的父表(简化示例):

CREATE TABLE [dbo].[Parent] (
    [Id] [uniqueidentifier] NOT NULL,
    [LookupId] [uniqueidentifier] NULL
)

CREATE TABLE [dbo].[Lookup] (
    [Id] [uniqueidentifier] NOT NULL,
    [Name] [nvarchar](64) NOT NULL
)
Run Code Online (Sandbox Code Playgroud)

在这种情况下,该Parent表有超过 1000 万行,而该Lookup表大约有 5000 行。真正的Parent实现有几个这样的对其他表的外键引用,并且这些列中的每一个都可能包含 NULL。

两个示例表的Id列都具有唯一的聚集索引,Parent具有 的非聚集索引LookupIdLookup的非聚集索引Name

我正在运行一个分页查询,我想在结果中包含查找值:-

SELECT
    P.Id,
    L.Name
FROM Parent P
LEFT JOIN Lookup L ON P.LookupId = L.Id 
ORDER BY P.Id
OFFSET 500000 ROWS FETCH NEXT 50 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

这运行得很快,按 排序也是如此P.LookupId

但是,如果我尝试按Name(或什至 )排序L.Id,则查询运行速度要慢得多:

SELECT
    P.Id,
    L.Name
FROM Parent P
LEFT JOIN Lookup L ON P.LookupId = L.Id 
ORDER BY L.Name
OFFSET 500000 ROWS FETCH NEXT 50 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

第二个查询的查询计划在这里:https : //www.brentozar.com/pastetheplan/?id=Sk3SIOvMD

其他看似相关的问题似乎涉及按第一个表中的列排序,这可以使用适当的索引来解决。

我尝试为此查询创建索引视图,但是,SQL Server 不允许我为视图建立索引,因为它包含我需要的 LEFT JOIN,因为它LookupId可能为 NULL,如果我使用 INNER JOIN,这些记录将被排除。

有没有办法优化这种情况?

编辑

Rob Farley 的回答(谢谢!)非常好,非常适合我最初提出的问题,其中我暗示我要加入一张桌子。

事实上,我有多个这样的表,我无法使用 INNER JOIN 来协调所有表以使用该解决方案。

目前,我通过向查找表添加一个“NULL”行来解决这个问题,这样我就可以使用 INNER JOIN 而不会丢失左侧的任何行。

就我而言,我使用uniqueidentifier身份,所以我创建了一个这样的索引视图:

CREATE VIEW [dbo].[ParentView]
WITH SCHEMABINDING
AS
SELECT
    P.Id,
    L.Name
FROM [dbo].Parent P
INNER JOIN [dbo].Lookup L ON ISNULL(P.LookupId, '00000000-0000-0000-0000-000000000000') = L.Id
Run Code Online (Sandbox Code Playgroud)

然后我向Lookup表中添加一行,其值为00000000-0000-0000-0000-000000000000for,Id因此连接的右侧总是有匹配项。

然后我可以根据需要在该视图上创建索引。

此外,由于我没有使用 Enterprise,我发现我需要使用NOEXPAND提示来确保使用这些索引:

SELECT *
FROM [ParentView]
WITH (NOEXPAND)
ORDER BY Name
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

Rob*_*ley 10

让我们首先考虑第一个查询。

您在 Parent 和 Lookup 之间加入,但它是一个外部联接,因此父项永远不会从结果中删除。我将猜测 Lookup.Id 是唯一的,因此,没有 Parent 将有多个 Lookup 加入。

因此,如果我们没有 OFFSET 子句,Parent 中的第 50000 行(按 Parent.Id 排序)将是结果中的第 50000 行。

因此,查询可以移动超过 50000 行的偏移量,查看接下来的 50 行,并使用它连接到查找表。如果连接没有找到任何东西并不重要,它是一个左外连接,它只会返回 NULL。

如果您按 Parent 中的不同列进行排序,并且该列已编入索引,则它可以同样快地移过这 50000 行。

现在让我们考虑第二个查询。

您希望您忽略的 50000 行(按偏移量)是基于连接结果的前 50000 行。这 50000 行可能包含一些 NULL,其中 Parent.LookupId 值在 Lookup 表中不存在。即使您在 Parent.LookupId 上有一个很好的索引,您也可能需要涉及大部分行,因为除非您发现 50050 行没有成功加入,否则您将需要继续下去。甚至 50050 也远远超过您在第一个查询中加入的 50 行。

现在,如果你有一个外键,那么事情可能会有所不同。然后,SQL 引擎应该知道,如果它有值,那么 Lookup.Name 不会为空。所以理论上它可以从查找空值开始,看看是否有 50000 个。但这仍然有点牵强,SQL 引擎不太可能产生这样的计划。

但你可以。

所以为了解决第二个查询的性能问题,我会做一些事情。

首先考虑不为空的那些。这意味着作为内部联接一部分的行。您可以对此进行索引视图,以便您可以按照您想要的顺序创建索引。

但是您还需要 Parent.LookupID 为空的那些 - 除了这些之外,您根本不需要连接。

如果您在这两个集合中执行 UNION ALL(并且可能在两个集合中都包含一个常量列,以确保 NULL 行出现在您的订单中的 NOT NULL 行之前),您应该能够看到一些改进。

像这样的东西:

SELECT ID, Name
FROM 
(
  SELECT i.ID, i.Name, 2 as SetNumber
  FROM dbo.MyIndexedView i
  UNION ALL
  SELECT p.ID, NULL, 1 as SetNumber
  FROM dbo.Parent p
  WHERE p.LookupID IS NULL
) u
ORDER BY u.SetNumber, u.Name
OFFSET 50000 ROWS FETCH NEXT 50 ROWS ONLY;
Run Code Online (Sandbox Code Playgroud)

希望您的计划将包括合并连接(串联)运算符,以便它仅从索引视图上的索引扫描(按名称顺序)和父级上的索引查找(对于 LookupID)提取所需的行。