CROSS APPLY 产生外连接

Pau*_*ite 18 sql-server execution-plan cross-apply

为了回答分区上不同的 SQL 计数, Erik Darling 发布了此代码以解决以下问题COUNT(DISTINCT) OVER ()

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;
Run Code Online (Sandbox Code Playgroud)

查询使用CROSS APPLY(not OUTER APPLY) 那么为什么在执行计划中有连接而不是连接?

在此处输入图片说明

另外,为什么取消注释 group by 子句会导致内部联接?

在此处输入图片说明

我不认为数据很重要,而是从 kevinwhat 在另一个问题上提供的数据中复制:

create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 24

概括

SQL Server 使用正确的连接(内部或外部)并在必要时添加投影,以在applyjoin之间执行内部转换遵守原始查询的所有语义

计划中的差异都可以通过SQL Server 中带和不带 group by 子句的聚合的不同语义来解释。


细节

加入 vs 申请

我们需要能够区分applyjoin

  • 申请

    的内部(下部)输入所适用运行的外(上)输入的每一行,与当前外行设置一个或多个内侧面的参数值。apply的整体结果是由参数化的内侧执行产生的所有行的组合(联合所有)。参数的存在意味着apply有时被称为相关连接。

    所适用的被执行计划总是实现嵌套循环操作。运算符将具有外部引用属性而不是连接谓词。外部引用是在循环的每次迭代中从外侧传递到内侧的参数。

  • 加入

    联接在联接运算符处评估其联接谓词。联接通常可以由SQL Server 中的Hash MatchMergeNested Loops运算符实现。

    嵌套循环被选择,它可以区分从一个应用受到缺乏外部引用(和通常的存在联接谓词)。的内部输入连接从外部输入从未引用值-内侧仍是一次为每个外部行执行,但内侧的执行不从目前的外排依赖于任何价值。

有关更多详细信息,请参阅我的帖子Apply 与 Nested Loops Join

...为什么在执行计划中有连接而不是连接?

当优化器将应用转换到连接(使用称为 的规则ApplyHandler)以查看它是否可以找到更便宜的基于连接的计划时,就会出现外连接。当应用包含标量聚合时,连接必须是外部连接以确保正确性。正如我们将看到的,内连接不能保证产生与原始应用相同的结果。

标量和向量聚合

  • 没有相应GROUP BY子句的聚合是标量聚合。
  • 具有相应GROUP BY子句的聚合是向量聚合。

在 SQL Server 中,标量聚合总是会产生一行,即使它没有被赋予要聚合的行。例如,COUNT没有行的标量聚合为零。甲矢量 COUNT没有行的骨料是空集(无任何行)。

以下玩具查询说明了差异。您还可以在我的文章Fun with Scalar and Vector Aggregates 中阅读有关标量和矢量聚合的更多信息。

-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;

-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
Run Code Online (Sandbox Code Playgroud)

db<>小提琴演示

转型申请加入

我之前提到过,当原始应用包含标量聚合时,为了正确性,连接必须是外连接。为了详细说明为什么会出现这种情况,我将使用问题查询的一个简化示例:

DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);

INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);

SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
Run Code Online (Sandbox Code Playgroud)

列正确的结果c,因为COUNT_BIG是一个标量总和。将此应用查询转换为连接表单时,SQL Server 会生成一个内部替代项,如果它在 T-SQL 中表示,则看起来类似于以下内容:

SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
    SELECT B.A, c = COUNT_BIG(*) 
    FROM @B AS B
    GROUP BY B.A
) AS J1
    ON J1.A = A.A;
Run Code Online (Sandbox Code Playgroud)

要将应用重写为不相关的联接,我们必须GROUP BY在派生表中引入 a (否则可能没有A要联接的列)。联接必须是外部联接,因此表中的每一行都会@A继续在输出中生成一行。当连接谓词的计算结果不为真时,左连接将生成一个NULLfor 列c。这NULL需要由被翻译成零COALESCE,完成从一个正确的转换申请

下面的演示展示了如何COALESCE使用join作为原始应用查询来生成相同的结果:

db<>小提琴演示

随着 GROUP BY

...为什么取消注释 group by 子句会导致内部联接?

继续简化示例,但添加一个GROUP BY

DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);

INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);

-- Original
SELECT * FROM @A AS A
CROSS APPLY 
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;

Run Code Online (Sandbox Code Playgroud)

现在COUNT_BIG是一个向量聚合,所以空输入集的正确结果不再是零,它根本不是行。换句话说,运行上面的语句不会产生任何输出。

当从apply转换为join 时,这些语义更容易遵守,因为CROSS APPLY自然会拒绝任何不生成内侧行的外部行。因此,我们现在可以安全地使用内部连接,无需额外的表达式投影:

-- Rewrite
SELECT A.*, J1.c 
FROM @A AS A
JOIN
(
    SELECT B.A, c = COUNT_BIG(*) 
    FROM @B AS B
    GROUP BY B.A
) AS J1
    ON J1.A = A.A;
Run Code Online (Sandbox Code Playgroud)

下面的演示显示了内连接重写产生的结果与使用向量聚合的原始应用产生的结果相同:

db<>小提琴演示

优化器碰巧选择了一个与小表的合并内连接,因为它很快找到了一个便宜的连接计划(找到了足够好的计划)。基于成本的优化器可能会继续将连接重写回应用程序 - 可能会找到更便宜的应用程序,因为如果使用循环连接或 forceseek 提示,它将在这里找到 - 但在这种情况下不值得付出努力。

笔记

简化示例使用不同内容的不同表格来更清楚地显示语义差异。

有人可能会争辩说,优化器应该能够推断自连接不能生成任何不匹配(非连接)的行,但它今天不包含该逻辑。无论如何,在查询中多次访问同一个表并不能保证产生相同的结果,这取决于隔离级别和并发活动。

优化器担心这些语义和边缘情况,因此您不必担心。


奖励:内部申请计划

SQL Server可以为示例查询生成一个内部应用计划(不是内部连接计划!),它只是出于成本原因选择不这样做。在我的笔记本电脑的 SQL Server 2017 实例上,问题中显示的外连接计划的成本是0.02898 个单位。

您可以使用未记录和不受支持的跟踪标志 9114(禁用等)强制应用(相关连接)计划,ApplyHandler仅用于说明:

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY 
(
    SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
    FROM   #MyTable AS mt2
    WHERE  mt2.Col_A = mt.Col_A 
    --GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Run Code Online (Sandbox Code Playgroud)

这会产生一个带有惰性索引假脱机的应用嵌套循环计划。总估计成本为0.0463983(高于所选计划):

索引假脱机应用计划

请注意,无论子句是否存在,使用应用嵌套循环的执行计划都会使用“内连接”语义产生正确的结果GROUP BY

在现实世界中,我们通常会有一个索引来支持应用内部的查找,以鼓励 SQL Server 自然地选择此选项,例如:

CREATE INDEX i ON #MyTable (Col_A, Col_B);
Run Code Online (Sandbox Code Playgroud)

db<>小提琴演示