考虑 SQL Server 2014 中的以下查询计划:
在查询计划中,自联接ar.fId = ar.fId
产生 1 行的估计值。然而,这是一个逻辑上不一致的估计:ar
有20,608
行和只有一个不同的值fId
(准确反映在统计数据中)。因此,此连接会生成行 (rows) 的完整叉积~424MM
,从而导致查询运行数小时。
我很难理解为什么 SQL Server 会得出一个很容易证明与统计数据不一致的估计值。有任何想法吗?
初步调查和其他细节
根据 Paul 在此处的回答,似乎用于估计连接基数的 SQL 2012 和 SQL 2014 启发式方法应该可以轻松处理需要比较两个相同直方图的情况。
我从跟踪标志 2363 的输出开始,但没那么容易理解。以下代码片段是否意味着 SQL Server 正在比较fId
和bId
的直方图以估计仅使用的联接的选择性fId
?如果是这样,那显然是不正确的。还是我误读了跟踪标志输出?
Plan for computation:
CSelCalcExpressionComparedToExpression( QCOL: [ar].fId x_cmpEq QCOL: [ar].fId )
Loaded histogram for column QCOL: [ar].bId from stats with id 3
Loaded histogram for column QCOL: [ar].fId from stats with id 1
Selectivity: 0
请注意,我已经提出了几种解决方法,它们包含在完整的重现脚本中,并将此查询缩短到毫秒。这个问题的重点是了解行为,如何在以后的查询中避免它,并确定它是否是应该向 Microsoft 提交的错误。
这是完整的重现脚本,这是跟踪标志 2363 的完整输出,这是查询和表定义,以防您想在不打开完整脚本的情况下快速查看它们:
WITH cte AS (
SELECT ar.fId,
ar.bId,
MIN(CONVERT(INT, ar.isT)) AS isT,
MAX(CONVERT(INT, tcr.isS)) AS isS
FROM #SQL2014MinMaxAggregateCardinalityBug_ar ar
LEFT OUTER JOIN #SQL2014MinMaxAggregateCardinalityBug_tcr tcr
ON tcr.rId = 508
AND tcr.fId = ar.fId
AND tcr.bId = ar.bId
GROUP BY ar.fId, ar.bId
)
SELECT s.fId, s.bId, s.isS, t.isS
FROM cte s
JOIN cte t
ON t.fId = s.fId
AND t.isT = 1
CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_ar (
fId INT NOT NULL,
bId INT NOT NULL,
isT BIT NOT NULL
PRIMARY KEY (fId, bId)
)
CREATE TABLE #SQL2014MinMaxAggregateCardinalityBug_tcr (
rId INT NOT NULL,
fId INT NOT NULL,
bId INT NOT NULL,
isS BIT NOT NULL
PRIMARY KEY (rId, fId, bId, isS)
)
一致性
没有一致性的一般保证。可以使用不同的统计方法在不同的时间对不同的(但逻辑上等效的)子树计算估计值。
连接这两个相同的子树应该产生叉积的逻辑没有错,但同样也没有什么可以说推理的选择比其他任何推理都更合理。
初步估计
在您的特定情况下,连接的初始基数估计不会在两个相同的子树上执行。当时的树形是:
第一个连接输入简化了未投影的聚合,第二个连接输入将谓词
t.isT = 1
推到其下方,其中t.isT
isMIN(CONVERT(INT, ar.isT))
。尽管如此,isT
谓词的选择性计算仍可用于CSelCalcColumnInInterval
直方图:(正确的)期望是 20,608 行被该谓词减少为 1 行。
加盟估价
现在的问题是如何将来自另一个连接输入的 20,608 行与这一行相匹配:
通常有几种不同的方法来估计连接。例如,我们可以:
根据使用的基数估计器和一些启发式方法,可以使用其中的任何一个(或变体)。有关更多信息,请参阅 Microsoft 白皮书使用 SQL Server 2014 基数估计器优化您的查询计划。
漏洞?
现在,如问题中所述,在这种情况下,“简单”单列连接(on
fId
)使用CSelCalcExpressionComparedToExpression
计算器:此计算评估将 20,608 行与第 1 个过滤行连接起来的选择性为零:没有行会匹配(在最终计划中报告为一行)。这是错误的吗?是的,这里可能是新 CE 中的错误。有人可能会争辩说 1 行将匹配所有行或不匹配,因此结果可能是合理的,但有理由相信并非如此。
细节实际上相当棘手,但估计基于未过滤
fId
直方图的期望,由过滤器的选择性修改,给20608 * 20608 * 4.85248e-005 = 20608
出行是非常合理的。按照此计算将意味着使用计算器
CSelCalcSimpleJoinWithDistinctCounts
而不是CSelCalcExpressionComparedToExpression
。没有记录的方法来执行此操作,但如果您好奇,可以启用未记录的跟踪标志 9479:请注意,最终连接从两个单行输入中生成 20,608 行,但这并不奇怪。它与 TF 9481 下的原始 CE 制定的计划相同。
我提到细节很棘手(调查起来很费时),但据我所知,问题的根本原因与 predicate 相关
rId = 508
,选择性为零。这个零估计以正常方式上升到一行,当它考虑输入树中的较低谓词时,这似乎有助于在有问题的连接处进行零选择性估计(因此加载 的统计信息bId
)。允许外部连接保持零行内侧估计(而不是提高到一行)(因此所有外部行都符合条件)可以使用任一计算器提供“无错误”连接估计。如果您有兴趣探索这个,未记录的跟踪标志是 9473(单独):
连接基数估计的行为
CSelCalcExpressionComparedToExpression
也可以修改为不考虑bId
另一个未记录的变体标志 (9494)。我提到所有这些是因为我知道你对这些事情感兴趣;不是因为他们提供了解决方案。在您向 Microsoft 报告问题并且他们解决(或不解决)之前,以不同方式表达查询可能是最好的前进方式。不管该行为是否有意,他们都应该有兴趣了解回归。最后,整理一下复制脚本中提到的另一件事:过滤器在问题计划中的最终位置是基于成本的探索
GbAggAfterJoinSel
将聚合和过滤器移动到连接上方的结果,因为连接输出具有如此小的行数。如您所料,筛选器最初位于连接下方。