SQL Server 索引视图和 TOP

Fru*_*aun 5 sql-server execution-plan view query-performance

我正在努力说服查询计划按照我认为应该的方式行事。在查询索引视图时添加 TOP 子句会导致次优计划,我希望在排序方面得到一些帮助。

环境

  • SQL Server 2019
  • StackOverflow2013 数据库(50GB 版本),Compat Mode 150(问题不是这个版本特有的)

设置:

首先,我创建了一个视图来回报每个人的高声誉:

CREATE VIEW vwHighReputation
WITH SCHEMABINDING
AS
SELECT  [Id],
        [DisplayName],
        [Reputation]
FROM    [dbo].[Users]
WHERE   [Reputation] > 10000
Run Code Online (Sandbox Code Playgroud)

接下来,由于我将按显示名称进行搜索,因此我在视图上创建了几个索引:

CREATE UNIQUE CLUSTERED INDEX IX_Users_Id ON [dbo].[vwHighReputation]([Id])
GO
CREATE NONCLUSTERED INDEX IX_Users_DisplayName ON [dbo].[vwHighReputation]([DisplayName]) INCLUDE (Reputation)
GO
Run Code Online (Sandbox Code Playgroud)

如果我通过视图查询,我可以看到我的非聚集索引正在被使用:

SELECT  *
FROM    [dbo].[vwHighReputation]
WHERE   [DisplayName] LIKE 'J%'
Run Code Online (Sandbox Code Playgroud)

计划:(https://www.brentozar.com/pastetheplan/?id=Sy2EoJaiv

到现在为止还挺好。我什至可以使用我的视图作为带有 OUTER APPLY 的更复杂查询的一部分,并且我仍然只对索引进行了 63 次读取(这显然是一个人为的示例,但有助于说明我将要解决的问题) ):

SELECT  [U].[Id],
        [A].[Reputation],
        [A].[DisplayName]
FROM    [dbo].[Users] AS [U]
        OUTER APPLY (
                        SELECT  *
                        FROM    [dbo].[vwHighReputation] AS [v]
                         WHERE   [v].[Id] = [U].[Id]
                    ) AS [A]
WHERE   [A].[DisplayName] LIKE 'J%'; 
Run Code Online (Sandbox Code Playgroud)

计划:https : //www.brentozar.com/pastetheplan/?id=HJaw3y6ov

但是,如果我将 TOP 1 添加到我的 OUTER APPLY:

SELECT  [U].[Id],
        [A].[Reputation],
        [A].[DisplayName]
FROM    [dbo].[Users] AS [U]
        OUTER APPLY (
                        SELECT  TOP 1 *
                        FROM    [dbo].[vwHighReputation] AS [v]
                        WHERE   [v].[Id] = [U].[Id]
                    ) AS [A]
WHERE   [A].[DisplayName] LIKE 'J%';
Run Code Online (Sandbox Code Playgroud)

然后情况变得很糟糕......非常非常糟糕......

计划:https : //www.brentozar.com/pastetheplan/?id=HyOS6yaiw

我针对该视图的逻辑读取计数现在接近 500 万。我可以从计划中看到 SQL Server 现在选择以用户 ID 作为谓词对聚集索引执行搜索,但这样做了大约 250 万次。它还扫描整个用户表。它不再寻找视图的索引。

显然优化器决定这是最有效的方法,但我不明白为什么!我认为这可能与底层表的排序方式有关,但我不确定。

顺便说一句,将其重写为简单的 SUB QUERY 而不是 CROSS APPLY 会产生相同的结果。

任何帮助或建议都会很棒!

Eri*_*ing 9

外敷

您正在使用OUTER APPLY, 但带有拒绝NULL值的 where 子句。

它被转换为没有以下内容的内部联接TOP (1)

SELECT  
    U.Id,
    A.Reputation,
    A.DisplayName
FROM dbo.Users AS U
OUTER APPLY 
(
    SELECT  
        v.*
    FROM dbo.vwHighReputation AS v
    WHERE v.Id = U.Id
) AS A
WHERE A.DisplayName LIKE 'J%'
ORDER BY U.Id;
Run Code Online (Sandbox Code Playgroud)

坚果

我已经稍微格式化了您的代码,并添加了一个ORDER BY来验证跨查询的结果。没有冒犯的意思。

外敷 + TOP (1)

当您使用 时TOP (1),连接是LEFT OUTER多种多样的:

SELECT  
    U.Id,
    A.Reputation,
    A.DisplayName
FROM dbo.Users AS U
OUTER APPLY 
(
    SELECT TOP (1)
        v.*
    FROM dbo.vwHighReputation AS v
    WHERE v.Id = U.Id
) AS A
WHERE A.DisplayName LIKE 'J%'
ORDER BY U.Id;
Run Code Online (Sandbox Code Playgroud)

坚果

TOP (1)内部OUTER APPLY显然使得优化器无法相同的变换应用到内部连接,即使有多余的谓词:

SELECT  
    U.Id,
    A.Reputation,
    A.DisplayName
FROM dbo.Users AS U
OUTER APPLY 
(
    SELECT TOP (1)
        v.*
    FROM dbo.vwHighReputation AS v
    WHERE v.Id = U.Id
    AND   v.DisplayName LIKE 'J%'
) AS A
WHERE A.DisplayName LIKE 'J%'
ORDER BY U.Id;
Run Code Online (Sandbox Code Playgroud)

坚果

请注意残差谓词以评估IdDisplayName列是否为NULL

这也不仅仅是一个TOP (1)问题——您可以替换任何不超过 big int max (9223372036854775807) 的值并查看相同的计划。

如果您完全跳过视图,也会发生这种情况。

SELECT  
    U.Id,
    A.Reputation,
    A.DisplayName
FROM dbo.Users AS U
OUTER APPLY 
(
    SELECT TOP (1)
        v.Id,
        v.DisplayName,
        v.Reputation
    FROM dbo.Users AS v
    WHERE v.Reputation > 10000 
    AND   v.Id = U.Id
) AS A
WHERE A.DisplayName LIKE 'J%'
ORDER BY U.Id
OPTION(EXPAND VIEWS);
Run Code Online (Sandbox Code Playgroud)

重写

获得与TOP (1)没有各种优化器副作用相同效果的一种方法TOP是使用ROW_NUMBER

SELECT  
    U.Id,
    A.Reputation,
    A.DisplayName
FROM dbo.Users AS U
OUTER APPLY 
(
    SELECT
        v.*
    FROM
    (
        SELECT 
            v.*,
            ROW_NUMBER() OVER 
            (
                PARTITION BY 
                    v.Id
                ORDER BY
                    v.Id
            ) AS n
        FROM dbo.vwHighReputation AS v
    ) AS v
    WHERE v.Id = U.Id
    AND   v.n = 1
) AS A
WHERE A.DisplayName LIKE 'J%'
ORDER BY U.Id;
Run Code Online (Sandbox Code Playgroud)

这将为您提供原始计划:

坚果