摘要
SQL Server使用正确的联接(内部或外部),并在执行apply和join之间的内部转换时在必要时添加投影,以尊重原始查询的所有语义。
计划中的差异全部可以通过SQL Server中带有和不带有group by子句的聚合的不同语义来解释。
细节
加入与申请
我们将需要能够区分Apply和Join:
应用
的内部(下部)输入所适用运行的外(上)输入的每一行,与当前外行设置一个或多个内侧面的参数值。的总的结果应用是全部由参数内侧执行所产生的行的组合(UNION ALL)。参数的存在意味着apply有时被称为关联联接。
一所适用的被执行计划总是实现嵌套循环操作。运算符将具有“ 外部引用”属性,而不是联接谓词。外部引用是每次循环迭代时从外侧传递到内侧的参数。
加入
联接在联接运算符处评估其联接谓词。联接通常可以由SQL Server中的Hash Match,Merge或Nested 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的正确结果c
是0,因为该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时,左连接将产生一个NULL
for列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演示