使用 union all 和 join 连接两个子查询会导致执行计划不理想

tuk*_*aef 6 performance sql-server execution-plan

我在生产环境中遇到了类似的问题,但我设法在Northwind DB上重现了这种行为。

考虑以下查询:

USE NORTHWND; 
DECLARE @ids TABLE ( Id NCHAR(50) );
INSERT  INTO @ids
VALUES  ( N'AROUT' ),
        ( N'ALFKI' );

SELECT  *
FROM    ( SELECT    c.CustomerID
          FROM      dbo.Customers c
          WHERE     c.CustomerID IN ( SELECT    Id
                                      FROM      @ids )
          UNION ALL
          SELECT    '0'
        ) t1
JOIN    ( SELECT    o.CustomerID
          FROM      dbo.Orders o
          --LEFT JOIN dbo.[Order Details] od ON o.OrderID = od.OrderID
          UNION ALL
          SELECT    '0'
        ) t2 ON t2.CustomerID = t1.CustomerID
OPTION  ( RECOMPILE );
Run Code Online (Sandbox Code Playgroud)

它有很好的查询计划: 好计划

现在,当我取消注释 line 时LEFT JOIN,计划变得不那么好(似乎查询处理器无法将CustomerID过滤器推送到数据访问运算符): 糟糕的计划

当我在FORCESEEK上添加提示时dbo.Orders,查询处理器无法生成查询计划。另一方面,当我删除其中一个时UNION ALL,查询计划变得和第一个一样好。

这是预期的行为吗?为什么查询处理器不在CustomerID连接运算符之前放置过滤器?

Geo*_*son 4

我认为 SQL Server 根本没有适当的优化规则来产生您在查询两侧都有Ordersa 的情况下正在寻找的查找查询。UNION ALL这样的查询计划理论上是可能的,但查询优化器无法为您的查询生成它。

为了得出这个结论,我将完整的原始查询(包括 )LEFT JOIN与查询的替代表述进行了比较,该查询的替代表述产生了低得多的估计成本(0.0420.085)。因此,如果 SQL Server 能够为您的查询探索此计划形状,它可能会选择这种成本较低的替代方案。

原始查询计划

这是最初的计划,预计成本为0.085

在此输入图像描述

备用查询计划

此查询在语义上与原始查询等效,但估计成本为0.042,大约是原始查询成本的一半。它是通过将第二个 UNION ALL 提升到查询的顶层而形成的。这要求我们两次引用t1(客户集),但即使如此,通过允许对Orders[Order Details]表进行搜索,也能以一半的成本产生一个计划。

在此输入图像描述

替代查询

以下是您可以用来尝试此方法的完整查询:

DECLARE @ids TABLE ( Id NCHAR(50) );
INSERT  INTO @ids
VALUES  ( N'AROUT' ),
        ( N'ALFKI' );

-- Define the original t1 as a CTE 
WITH customers AS (
    SELECT    c.CustomerID
    FROM      dbo.Customers c
    WHERE     c.CustomerID IN ( SELECT    Id
                                FROM      @ids )
    UNION ALL
    SELECT    '0'
)
-- Join t1 to the top half of the original UNION ALL
SELECT  *
FROM Customers t1
JOIN ( SELECT    o.CustomerID
          FROM      dbo.Orders o
          LEFT JOIN dbo.[Order Details] od ON o.OrderID = od.OrderID
        ) t2 ON t2.CustomerID = t1.CustomerID
-- And then join it again to the bottom half of the original UNION ALL
UNION ALL
SELECT  *
FROM Customers t1
JOIN ( SELECT    '0' AS CustomerId ) t2 ON t2.CustomerID = t1.CustomerID
OPTION  ( RECOMPILE);
Run Code Online (Sandbox Code Playgroud)

要点/注意事项

  • 一般来说,我发现Concatenation运算符很擅长妨碍查询优化器并阻止它产生最佳计划。几个例子包括这个 Connect 问题,其中UNION ALL阻止了优化的位图过滤器以及Concatenation操作符的重复观察,特别是在嵌套的情况下,欺骗了 SQL 2012 和早期的基数估计器,并且可能由于较差的基数估计而产生次优计划。显然UNION ALL这是一个非常有用的工具,但值得注意的是它偶尔会限制查询优化器优化查询的能力。
  • 实际上,以这种方式将查询拆分为两个单独的单元对于您的实际用例可能有意义,也可能没有意义,但值得一试。(这个特定的查询受益于这样一个事实:两个块之一可以被简化,因为它只包含虚拟SELECT '0'。)
  • 涉及的数据集Northwind非常小,很难得出结论,因此使用现实世界的数据集进行测试非常重要。