为什么这个连接基数估计值这么大?


18

我遇到以下查询的基数估计过高:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

估计的计划在这里。我正在处理表的统计副本,因此无法包含实际计划。但是,我认为这与这个问题无关。

SQL Server估计将从“ dm”派生表中返回481577行。然后,它估计在执行到X_LAST_TABLE的联接之后将返回4528030000行,但是JOIN_ID是X_LAST_TIME的主键。我期望连接基数估计介于0和481577行之间。相反,行估计似乎是交叉连接外部表和内部表时获得的行数的10%。此数学运算采用四舍五入方法:481577 * 94025 * 0.1 = 45280277425,其舍入为4528030000。

我主要是在寻找这种行为的根本原因。我也对简单的解决方法感兴趣,但是请不要建议更改数据模型或使用临时表。该查询是视图内逻辑的简化。我知道在几列上进行COALESCE并加入它们并不是一个好习惯。这个问题的部分目标是弄清楚我是否需​​要建议重新设计数据模型。

我正在启用旧基数估计器的Microsoft SQL Server 2014上进行测试。TF 4199和其他设备已打开。如果最终相关,我可以提供跟踪标志的完整列表。

这是最相关的表定义:

CREATE TABLE X_LAST_TABLE (
JOIN_ID NUMERIC(18, 0) NOT NULL
    CONSTRAINT PK_X_LAST_TABLE PRIMARY KEY CLUSTERED (JOIN_ID ASC)
);

我还编写了所有表创建脚本及其统计信息的脚本如果有人想在其一台服务器上重现该问题,。

为了补充我的一些看法,使用TF 2312可以修正估计值,但这对我来说不是一个选择。TF 2301无法确定估算值。删除其中一张表即可修正估算值。奇怪的是,更改X_DETAIL_LINK的连接顺序也会固定估计值。通过更改连接顺序,我的意思是重写查询,而不用提示强制连接顺序。这是仅更改联接顺序时的估计查询计划


PS:如果您可以通过任何方式切换到bigint而不是,decimal(18, 0)您将获得好处:1)使用8个字节而不是每个值使用9个字节,2)使用字节可比数据类型而不是打包数据类型,这可能会产生影响比较值时的CPU时间。
ErikE

@ErikE感谢您的提示,但我已经知道了。不幸的是,由于遗留原因,我们在BIGINT上仍然使用NUMERIC(18,0)。
Joe Obbish

值得一试!
ErikE

如果中的in不为null,是否需要X_DETAIL2and X_DETAIL3表?JOIN_IDX_DETAIL1
ErikE

@ErikE这是一个MCVE,因此此时查询并不完全有意义。
Joe Obbish

Answers:


14

我知道COALESCE在几列上进行合并不是一个好习惯。

当模式为3NF +(具有键和约束)并且查询是关系式且主要是SPJG(选择-投影-联接-分组依据)时,生成良好的基数和分布估计值将非常困难。CE模型基于这些原则。更不寻常的或者非关系的特点有一个查询时,愈接近什么样的基数和选择性框架可以处理边界。走的太远,CE会放弃猜测

大多数MCVE示例都是简单的SPJ(无G),尽管主要是外部等连接(建模为内部连接加反半连接),而不是更简单的内部等连接(或半连接)。所有关系都有键,尽管没有外键或其他约束。除其中一个联接外,其他联接都是一对多的,这很好。

和之间的多对多外部联接是一个例外。MCVE中此联接的唯一功能是可能会复制中的行。这是一件不寻常的事情。X_DETAIL_1X_DETAIL_LINKX_DETAIL_1

简单的相等谓词(选择)和标量运算符也更好。例如,属性compare-equal属性/常数在模型中通常可以很好地工作。修改直方图和频率统计信息以反映此类谓词的应用相对“容易”。

COALESCE是基于构建的CASE,而该构建又在内部实现IIF(这IIF在Transact-SQL语言中出现之前确实是正确的)。CE建模IIFUNION具有两个互斥子级的子级,每个子级都包含一个有关输入关系选择的项目。列出的每个组件都具有模型支持,因此将它们组合起来相对简单。即便如此,抽象层越多,最终结果往往越不准确-这就是较大的执行计划趋于不稳定和可靠的原因。

ISNULL另一方面,是引擎固有的。它不是使用任何其他基本组件构建的。ISNULL例如,对直方图应用效果就像替换NULL值的步骤一样简单(并根据需要压缩)。随着标量运算符的发展,它仍然相对不透明,因此最好避免使用。尽管如此,与CASE基于替代方案的替代方案相比,与优化器更为友好(更少的对优化器不利)。

即使按照SQL Server标准,CE(70和120+)也非常复杂。并非向每个运算符应用简单逻辑(带有秘密公式)的情况。CE知道密钥和功能依赖性。它知道如何使用频率,多元统计量和直方图进行估算;并且有大量的特殊情况,改进,制衡与支持结构。它通常以多种方式(例如频率,直方图)估算联接,并根据两者之间的差异决定结果或调整。

最后要介绍的基本内容:初始基数估算从下至上针对查询树中的每个操作运行。首先针对叶运算符(基关系)导出选择性和基数。修改后的直方图和密度/频率信息是为父运算符导出的。我们走的树越远,估计的质量就越低,因为误差容易累积。

这个单一的初始全面估计提供了一个起点,并且在考虑最终执行计划之前就已经发生了(它甚至在琐碎的计划编制阶段之前就已经发生了)。此时的查询树倾向于相当紧密地反映查询的书面形式(尽管删除了子查询,并进行了简化等)。

初步估计后,SQL Server立即执行启发式联接重排序,从广义上讲,它尝试对树进行重排序以放置较小的表,并首先进行高选择性联接。它还尝试将内部联接放置在外部联接和叉积之前。它的功能不广泛;它的努力并不详尽;并且不考虑实际成本(因为它们尚不存在-仅存在统计信息和元数据信息)。启发式重排序在简单的内部等分树上最成功。它的存在为基于成本的优化提供了“更好”的起点。

为什么这个连接基数估计值这么大?

MCVE 在谓词中具有一个“不寻常”的,主要是冗余的多对多连接,以及一个等值连接COALESCE。运算符树还具有最后一个内部联接,该启发式联接重新排序无法将树向上移动到更喜欢的位置。撇开所有标量和投影,连接树为:

LogOp_Join [ Card=4.52803e+009 ]
    LogOp_LeftOuterJoin [ Card=481577 ]
        LogOp_LeftOuterJoin [ Card=481577 ]
            LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
                LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
                LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
            LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
        LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
    LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]

请注意,错误的最终估算值已经到位。它Card=4.52803e+009以双精度浮点值4.5280277425e + 9(十进制4528027742.5)的形式打印并在内部存储。

原始查询中的派生表已删除,并且投影已标准化。执行初始基数和选择性估计的树的SQL表示为:

SELECT 
    PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
    ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk 
    ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2 
    ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3 
    ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst 
    ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)

(顺便说一句,COALESCE在最终计划中也存在重复项-一次出现在最终的Compute Scalar中,一次出现在内部联接的内侧)。

注意最后的联接。此内部联接(根据定义)是的笛卡尔乘积X_LAST_TABLE和先前的联接输出,选择项(联接谓词)是lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)。笛卡尔积的基数仅为481577 * 94025 = 45280277425。

为此,我们需要确定并应用谓词的选择性。不透明展开COALESCE树的组合(根据UNIONIIF记住)的组合以及对早期关键性冗余的多对多外部连接的关键信息,派生直方图和频率的影响,意味着CE无法以任何正常方式得出可接受的估计。

结果,它进入猜测逻辑。猜测逻辑相当复杂,尝试了“受过教育”的猜测和“未经受过教育”的猜测算法。如果找不到更好的猜测依据,则该模型将使用万不得已的猜测,对于相等比较而言:sqllang!x_Selectivity_Equal=固定为0.1选择性(猜测为10%):

调用堆栈

-- the moment of doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal

结果是笛卡尔积的0.1选择性:481577 * 94025 * 0.1 = 4528027742.5(〜4.52803e + 009),如上所述。

改写

注释掉有问题的联接时,由于避免了固定选择的“万不得已的猜测”(关键信息由1-M联接保留),因此可以产生更好的估计。估计的质量仍然是低置信度,因为COALESCE连接谓词根本不是CE友好的。我想,修改后的估算至少对人类而言看起来更合理。

当使用外部联接将查询写入X_DETAIL_LINK 最后时,启发式重排序可以将其与最终的内部联接交换X_LAST_TABLE。把内部连接旁边的问题外连接提供了早期的重新排序的能力有限,以提高最终估计的机会,因为大多是冗余的“不寻常”的影响很多一对多外连接进来之后棘手的选择性估计为COALESCE。同样,这些估计值比固定的猜测要好一点,并且可能不会经受法院确定的盘问。

重新排列内部联接和外部联接的混合是困难且耗时的(即使第2阶段的完全优化也仅尝试了理论动作的有限子集)。

ISNULL马克斯·弗农(Max Vernon)的答案中建议的嵌套方法设法避免了纾困的固定猜测,但最终的估计值是不可能的零行(为了体面而提高到一行)。对于计算具有的所有统计基础,这也可能是对1行的固定猜测。

我期望连接基数估计介于0和481577行之间。

即使有人接受基数估计可以在物理上不同但逻辑上和语义上相同的子树上(在基于成本的优化过程中)在不同时间进行(最终计划是最好的结合在一起),这是一个合理的期望。最佳(每个备忘录组)。缺乏计划范围的一致性保证并不意味着每个人都应该能够表现出尊重。

另一方面,如果我们最终只能猜到最后的办法,希望已经消失了,那为什么还要麻烦呢。我们尝试了所有已知的技巧,然后放弃了。如果没有其他问题,那么疯狂的最终估计是一个很好的警告信号,表明在此查询的编译和优化过程中,并非所有内容都在CE内部运行良好。

当我尝试使用MCVE时,120 + CE ISNULL为原始查询生成了零(= 1)行最终估计值(如nested ),这也是我的思维方式所不能接受的。

真正的解决方案可能涉及设计更改,以允许不带COALESCE或的简单等值连接ISNULL,以及理想情况下对查询编译有用的外键和其他约束。


10

我相信,由于加入而Compute Scalar产生的运营商是问题的根本原因。从历史上看,计算标量已经难以准确地花费12COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)X_LAST_TABLE.JOIN_ID

由于您提供了一个具有准确统计信息的最少完整的可验证示例(谢谢!),因此我可以重写查询,以便联接不再需要扩展到其中的CASE功能COALESCE,从而获得更准确的行估计,并且显然更多准确的总体成本核算请参阅附录。

SELECT COALESCE(dm.d1ID, dm.d2ID, dm.d3ID)
FROM
(
    SELECT d1ID = d1.JOIN_ID
        , d2ID = d2.JOIN_ID
        , d3ID = d3.JOIN_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst 
    ON (dm.d1ID IS NOT NULL AND dm.d1ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NOT NULL AND dm.d2ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NULL AND dm.d3ID IS NOT NULL AND dm.d3ID = lst.JOIN_ID);

尽管从xID IS NOT NULL技术上讲不是必需的,但由于ID = JOIN_ID不会将null值联接在一起,因此我将它们包括在内,因为它可以更清楚地描述其意图。

计划1计划2

方案1:

在此处输入图片说明

方案2:

在此处输入图片说明

新查询受益于并行化。还要注意,新查询的估计输出行数为1,实际上,到一天结束时,它可能比原始查询的估计值4528030000更糟。新查询中select运算符的子树成本为243210,而原始查询的成本为536.535,这显然要少得多。话虽如此,我不认为第一个估计值接近现实。


附录1。

在与@Lamak进行讨论后,就有关Heap™的问题与各界人士进行了进一步协商之后,即使在并行性方面,我上面的观察性查询似乎也表现出色。允许既有良好性能的解决方案良好的基数估计包括更换的COALESCE(x,y,z)ISNULL(ISNULL(x, y), z),如:

SELECT dm.PRIMARY_ID
FROM
(
    SELECT ISNULL(ISNULL(d1.JOIN_ID, d2.JOIN_ID), d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

COALESCECASE由查询优化器转换为“在幕后”的语句。这样,基数估计器很难发现埋在其中的列的可靠统计信息COALESCEISNULL对于基数估计器来说,作为一个内在函数要“开放”得多。ISNULL如果已知目标不可为空,那么就没有什么可以优化的东西了。

ISNULL变体的计划如下所示:

在此处输入图片说明

在此处粘贴计划版本)。

仅供参考,Sentry One出色的计划资源管理器的道具,我曾用它来生成上面的图形计划。


-1

根据您的加入条件,表可以以多种方式排列,即“改变为一种特定的方式”来固定结果。

假设仅连接一张表将为您提供正确的结果。

SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID

在这里X_DETAIL_1,您可以使用X_DETAIL_2X_DETAIL_3

因此,剩下的2张桌子的目的不清楚。

就像您将表格X_DETAIL_1分为两部分一样。

最有可能“ 有你在哪里填充这些表的错误。 ”理想的情况下X_DETAIL_1X_DETAIL_2X_DETAIL_3应包含的行等量。

但是一个或多个表包含不希望有的行数。

对不起,如果我错了。

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.