设置
DROP TABLE IF EXISTS #EmptyTable, #BigTable
CREATE TABLE #EmptyTable(A int);
CREATE TABLE #BigTable(A int);
INSERT INTO #BigTable
SELECT TOP 10000000 CRYPT_GEN_RANDOM(3)
FROM sys.all_objects o1,
sys.all_objects o2,
sys.all_objects o3;
询问
WITH agg
AS (SELECT DISTINCT a
FROM #BigTable)
SELECT *
FROM #EmptyTable E
INNER HASH JOIN agg B
ON B.A = E.A;
执行计划
问题
这是我今天之前没有注意到的现象的简化重现。我对内部散列连接的期望是,如果构建输入为空,则不应执行探测端,因为连接不会返回任何行。上面的示例与此相矛盾,并从表中读取了 1000 万行。这使查询的执行时间增加了 2.196 秒 (99.9%)。
额外的观察
- 执行
OPTION (MAXDOP 1)
计划没有从中读取任何行。适用于散列连接内部的所有运算符#BigTable
。ActualExecutions
0
- 对于查询
SELECT * FROM #EmptyTable E INNER HASH JOIN #BigTable B ON B.A = E.A
——我得到一个并行计划,散列连接内部的扫描运算符确实有ActualExecutions
DOP,但仍然没有读取任何行。该计划没有重新分区流运算符(或聚合)
问题
这里发生了什么?为什么原始计划会出现问题而其他情况却不会?
当构建为空时不运行连接的探测端是一种优化。当探测端有子分支时,即当有交换运算符时,它不适用于并行行模式散列连接。
许多年前,Adam Machanic 在现已关闭的 Connect 反馈网站上发表了类似的报告。该场景是探针端的启动过滤器,它意外地运行了它的子操作符。Microsoft 的回答是,引擎需要保证某些结构已初始化,唯一明智的强制执行方式是确保打开探测端运算符。
我自己对细节的回忆是,不初始化子树会导致难以修复的并行时序错误。确保子分支启动是解决这些问题的一种方法。
批处理模式哈希连接没有这种副作用,因为管理线程的方式不同。
在您的特定情况下,效果更为明显,因为哈希聚合正在阻塞;它在迭代器的 Open() 调用期间消耗了它的全部输入。当探测端只有流式运算符时,性能影响通常会更有限,具体取决于将第一行返回到散列连接的探测端所需的工作量。
不是答案,但如果不强制执行散列连接,则该查询不会将散列连接作为计划。解决方法是如果表中存在行则将位变量设置为 1,如果不存在则设置为 0,而不是使用#Emptytable (Select * from #Emptytable where @bit =1)
并在最后添加一个选项 recompile ,不会执行。
我认为如果不使用强制并且如果需要强制存在解决方法,那么这种情况永远不会发生。