交叉应用产生外部连接


17

为了回答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;

查询使用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)

Answers:


23

摘要

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

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


细节

加入与申请

我们将需要能够区分ApplyJoin

  • 应用

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

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

  • 加入

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

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

有关更多详细信息,请参见我的文章Apply vs Nested Loops Join

...为什么执行计划中有外部联接而不是内部联接?

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

标量和矢量聚合

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

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

以下玩具查询说明了区别。您还可以在我的文章“ 与标量和矢量聚合有趣”中阅读有关标量和矢量聚合的更多信息。

-- 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 ();

db <> fiddle演示

转型申请加入

我之前提到过,当原始应用包含标量集合时,为了正确起见,必须将联接作为外部联接。为了详细说明为什么是这种情况,我将使用问题查询的简化示例:

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;

column的正确结果c0,因为该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;

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

下面的演示显示了如何使用外部联接以及COALESCE使用联接产生与原始Apply查询相同的结果:

db <> fiddle演示

随着 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;

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;

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

db <> fiddle演示

优化器碰巧选择了带有小表的合并内部联接,因为它会快速找到便宜的联接计划(找到足够好的计划)。基于成本的优化器可能会继续将联接重写回应用程序-也许找到一个更便宜的应用程序计划,就像在这里使用循环联接或forceeekeek提示时那样-但是在这种情况下不值得付出努力。

笔记

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

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

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


奖金:内部申请计划

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);

这产生适用嵌套循环计划与懒惰索引卷轴上。估计总费用为0.0463983(高于所选计划):

索引假脱机申请计划

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

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

CREATE INDEX i ON #MyTable (Col_A, Col_B);

db <> fiddle演示


-3

交叉应用是对数据的逻辑操作。在决定如何获取该数据时,SQL Server选择适当的物理运算符来获取所需的数据。

没有物理应用运算符,SQL Server会将其转换为适当且希望有效的联接运算符。

您可以在下面的链接中找到物理操作员的列表。

https://docs.microsoft.com/zh-cn/sql/relational-databases/showplan-ologic-and-physical-operators-reference?view=sql-server-2017

查询优化器将查询计划创建为由逻辑运算符组成的树。查询优化器创建计划后,查询优化器为每个逻辑运算符选择最有效的物理运算符。查询优化器使用基于成本的方法来确定哪个物理运算符将实现逻辑运算符。

通常,逻辑运算可以由多个物理运算符实现。但是,在极少数情况下,物理操作员也可以实现多个逻辑操作。

编辑/似乎我理解你的问题是错误的。SQL Server通常会选择最合适的运算符。您的查询不需要返回两个表的所有组合的值,这是将使用交叉联接的时候。仅计算每一行所需的值就足够了,这就是在这里完成的工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.