在我看来,如何实现谓词下推

Dan*_*her 5 optimization sql-server-2019 query-performance

我有一个报告表(大约 10 亿行)和一个很小的维度表:

CREATE TABLE dbo.Sales_unpartitioned (
    BusinessUnit    int NOT NULL,
    [Date]          date NOT NULL,
    SKU             varchar(8) NOT NULL,
    Quantity        numeric(10, 2) NOT NULL,
    Amount          numeric(10, 2) NOT NULL,
    CONSTRAINT PK_Sales_unpartitioned PRIMARY KEY CLUSTERED (BusinessUnit, [Date], SKU)
);

--- Demo data:
INSERT INTO dbo.Sales_unpartitioned
SELECT severity AS BusinessUnit,
       DATEADD(day, message_id, '2000-01-01') AS [Date],
       LEFT([text], 3) AS SKU,
       1000.*RAND(CHECKSUM(NEWID())) AS Quantity,
       10000.*RAND(CHECKSUM(NEWID())) AS Amount
FROM sys.messages
WHERE [language_id]=1033;

--- Artificially inflate statistics of demo data:
UPDATE STATISTICS dbo.Sales_unpartitioned WITH ROWCOUNT=1000000000;

--- Dimension table:
CREATE TABLE dbo.BusinessUnits (
    BusinessUnit    int NOT NULL,
    SalesManager    nvarchar(250) NULL,
    PRIMARY KEY CLUSTERED (BusinessUnit)
);

INSERT INTO dbo.BusinessUnits (BusinessUnit)
SELECT DISTINCT BusinessUnit FROM dbo.Sales;
Run Code Online (Sandbox Code Playgroud)

...我添加了一个应用程序使用的报告视图,用于 OLTP 样式的报告。

CREATE OR ALTER VIEW dbo.SalesReport_unpartitioned
AS

SELECT bu.BusinessUnit,
       s.[Date],
       s.SKU,
       s.Quantity,
       s.Amount
FROM dbo.BusinessUnits AS bu
CROSS APPLY (
    --- Regular sales
    SELECT t.BusinessUnit, t.[Date], t.SKU, t.Quantity, t.Amount
    FROM dbo.Sales_unpartitioned AS t
    WHERE t.BusinessUnit=bu.BusinessUnit
      AND t.SKU LIKE 'T%'

    UNION ALL

    --- This is a special reporting entry. We only
    --- want to see today's row. In case of duplicates,
    --- get the row with the first "SKU".
    SELECT TOP (1) s.BusinessUnit, s.[Date], s.SKU, s.Quantity, s.Amount
    FROM dbo.Sales_unpartitioned AS s
    WHERE s.BusinessUnit=bu.BusinessUnit
      AND s.[Date]=CAST(SYSDATETIME() AS date)
      AND s.SKU LIKE 'S%'
    ORDER BY s.BusinessUnit, s.[Date], s.SKU
) AS s
Run Code Online (Sandbox Code Playgroud)

这个想法是用户应用程序将使用 SELECT 查询来查询这个视图,该查询过滤了一系列日期和一个或多个 BusinessUnit。为此,我选择了一种CROSS APPLY模式,以便查询可以在每个业务单元上“循环”,寻找日期范围,并对 SKU 应用残差过滤器。

示例应用查询:

DECLARE @from date='2021-01-01', @to date='2021-12-31';

SELECT *
FROM dbo.SalesReport_unpartitioned
WHERE BusinessUnit=16
  AND [Date] BETWEEN @from AND @to
ORDER BY BusinessUnit, [Date], SKU;
Run Code Online (Sandbox Code Playgroud)

我希望查询计划如下所示: 具有谓词下推且无过滤器运算符的所需查询计划 想要的计划

然而,计划结果是这样的: 实际查询计划仅在 SKU 上有残差谓词的 BusinessUnit 上查找,在计划末尾添加 Filter 实际计划

我希望 SQL Server 对日期列执行“谓词下推”,允许聚集索引查找查找单个业务单位和日期范围,然后在 SKU 上应用剩余谓词。这适用于“s”分支(带有TOP)的 Seek - 可能是因为它在查询中具有硬编码的 Date 谓词 - 但不适用于“t”分支。

但是,在“t”分支上,SQL Server 仅通过 SKU 上的剩余谓词寻找特定的 BusinessUnit,从而有效地检索所有日期。只有在计划结束时,它才会应用过滤器运算符来过滤日期列。

在一个大表中,这会带来非常显着的性能损失 - 当您只需要一周时,您最终可能会从磁盘读取 20 年的数据。

我尝试过的事情

解决方法:

  • 使用过滤“s”和“t”查询的@fromDate 和@toDate 参数将视图转换为内联表值函数将根据需要启用 Seek on (BusinessUnit, Date),但需要重写应用程序代码。
  • 移动UNION ALL出来的CROSS APPLY(从CROSS APPLY (UNION)CROSS APPLY() UNION CROSS APPLY())将使谓词下推。它在 BusinessUnit 表上再进行一次查找,这是完全可以接受的。

修复了 Seek,但更改了结果:

  • 令人惊讶的是,删除"s" 查询的TOP (1)andORDER BY会使谓词下推对 "t" 起作用,但可能会从 "s" 返回太多行。
  • UNION ALL通过删除“s”或“t”查询来消除将启用谓词下推,但会生成不正确的结果。

没有改变或不可行:

  • TOP (1)ROW_NUMBER()模式替换不会改变 Seek。
  • 将 the 更改CROSS APPLY为强制INNER LOOP JOIN修复了“t”上的 Seek,但实际上将“s”更改为 Scan,这甚至更糟。
  • 添加跟踪标志 8780 以允许优化器在计划上工作更长时间不会改变任何内容。该计划已经完全优化,没有提前终止。

一个常见的思路似乎是更改/简化“s”查询(删除TOP, ORDER BY)修复了“t”查询上的问题,这对我来说是违反直觉的。

我在找什么

我试图了解这是否是优化器的缺点,是否是故意成本计算/优化机制的结果,或者我是否只是忽略了某些东西。

Pau*_*ite 8

我试图了解这是否是优化器的缺点,是否是故意成本计算/优化机制的结果,或者我是否只是忽略了某些东西。

这是所有这些的一点点。

所提出的查询中发生了很多事情——真的太多了——所以为了避免写半本书,我将把它归结为导致你没有得到你所追求的计划的主要因素:

优化器不会将谓词下推到应用的内侧。

上的关系的选择(过滤器,谓词)操作一个以上的规则应用被调用时,很自然地,SELonApply。它执行以下逻辑替代:

Sel (A 申请 B) -> Sel (Sel A 申请 B)

它接受涉及 A 和 B 的潜在复杂选择的一部分,并将那些它可以的部分推送到驱动表 A。没有选择的部分被推送到 B。选择的部分不能被推倒留在后面。


这听起来像是一个令人震惊的疏忽,与经验背道而驰。那是因为它不是完整的故事。

优化器尝试在编译过程的早期(在简化期间,在琐碎计划和基于成本的优化之前)将应用转换为等效的连接。它能够在安全的地方将选择推到join 的任一侧。在基于成本的优化过程中,该连接又可以转化为物理应用

所有这些的效果是让优化器看起来像是将谓词推到了应用的内侧:

  1. 书面申请转换为连接。
  2. 谓词下推连接的任一侧。
  3. 加入转化为申请。

让我给你看一个例子:

DECLARE @T1 table (pk integer PRIMARY KEY, c1 integer NOT NULL INDEX ic1);
DECLARE @T2 table (fk integer NOT NULL, c2 integer NOT NULL, PRIMARY KEY (fk, c2));

SELECT 
    T1.*,
    T2.*
FROM @T1 AS T1
CROSS APPLY 
(
    SELECT T2.* 
    FROM @T2 AS T2
    WHERE T2.fk = T1.pk
) AS T2
WHERE 
    1 = 1
    AND T1.c1 = 1
    AND T2.c2 = 2;
Run Code Online (Sandbox Code Playgroud)

下推谓词的计划

如果仔细查看计划,您会看到 T2 上的谓词被推到了内部查找,并且嵌套循环连接是一个应用(它有外部引用)。这是唯一可能的,因为优化器最初能够将应用重写为连接,推送谓词,然后稍后转换回应用。

我们可以使用未记录的跟踪标志 9114 禁用 apply-to-join 重写:

DECLARE @T1 table (pk integer PRIMARY KEY, c1 integer NOT NULL INDEX ic1);
DECLARE @T2 table (fk integer NOT NULL, c2 integer NOT NULL, PRIMARY KEY (fk, c2));

SELECT 
    T1.*,
    T2.*
FROM @T1 AS T1
CROSS APPLY 
(
    SELECT T2.* 
    FROM @T2 AS T2
    WHERE T2.fk = T1.pk
) AS T2
WHERE 
    1 = 1
    AND T1.c1 = 1
    AND T2.c2 = 2
OPTION (QUERYTRACEON 9114);
Run Code Online (Sandbox Code Playgroud)

这意味着only SELonApplycan be used,只推送到驱动表A:

带有卡住谓词的计划

请注意,T2.c2 上的选择部分“卡在”过滤器中的应用上方。(内侧查找仅在 apply 中指定的 fk/pk 相等性上。)


优化器建立在关系原则之上。它欣赏关系模式设计和使用关系构造的查询。Apply(横向连接)是一个相对较新的扩展。优化器在 join 方面知道更多的技巧,它在 apply 方面做了很多,因此早期努力重写。

当您使用诸如 apply 或非关系 Top 之类的东西时,您隐含地对最终计划形状承担了更多责任。换句话说,您将更多地需要以不同的方式表达您的查询(如在您的解决方法中)以获得良好的结果。


作为记录,我更喜欢使用带有显式谓词放置的内联表值函数。如果我要重写视图,我可能会选择:

CREATE OR ALTER VIEW dbo.SalesReport_unpartitioned
AS
--- Regular sales
SELECT
    BU.BusinessUnit,
    RS.[Date],
    RS.SKU,
    RS.Quantity,
    RS.Amount
FROM dbo.BusinessUnits AS BU
JOIN dbo.Sales_unpartitioned AS RS
    ON RS.BusinessUnit = BU.BusinessUnit
WHERE 
    RS.SKU LIKE 'T%'

UNION ALL

--- This is a special reporting entry.
SELECT
    BU.BusinessUnit,
    SR.[Date],
    SR.SKU,
    SR.Quantity,
    SR.Amount
FROM dbo.BusinessUnits AS BU
JOIN dbo.Sales_unpartitioned AS SR
    ON SR.BusinessUnit = BU.BusinessUnit
WHERE
    1 = 1
    AND SR.SKU LIKE 'S%'
    --- We only want to see today's row.
    AND SR.[Date] = CONVERT(date, SYSDATETIME())
    --- In case of duplicates, get the row with the first "SKU".
    AND SR.SKU =
    (
        SELECT 
            MIN(SR2.SKU) 
        FROM dbo.Sales_unpartitioned AS SR2
        WHERE 
            SR2.BusinessUnit = SR.BusinessUnit
            AND SR2.[Date] = SR.[Date]
            AND SR2.SKU LIKE 'S%'
    );
GO
Run Code Online (Sandbox Code Playgroud)

对于提供的测试查询:

DECLARE @from date='2021-01-01', @to date='2021-12-31';

SELECT *
FROM dbo.SalesReport_unpartitioned
WHERE BusinessUnit=16
  AND [Date] BETWEEN @from AND @to
ORDER BY BusinessUnit, [Date], SKU;
Run Code Online (Sandbox Code Playgroud)

执行计划是:

视图重写计划

橙色部分为常规销售。黄色部分用于特殊报告条目。