我知道COALESCE
在几列上进行合并不是一个好习惯。
当模式为3NF +(具有键和约束)并且查询是关系式且主要是SPJG(选择-投影-联接-分组依据)时,生成良好的基数和分布估计值将非常困难。CE模型基于这些原则。更不寻常的或者非关系的特点有一个查询时,愈接近什么样的基数和选择性框架可以处理边界。走的太远,CE会放弃猜测。
大多数MCVE示例都是简单的SPJ(无G),尽管主要是外部等连接(建模为内部连接加反半连接),而不是更简单的内部等连接(或半连接)。所有关系都有键,尽管没有外键或其他约束。除其中一个联接外,其他联接都是一对多的,这很好。
和之间的多对多外部联接是一个例外。MCVE中此联接的唯一功能是可能会复制中的行。这是一件不寻常的事情。X_DETAIL_1
X_DETAIL_LINK
X_DETAIL_1
简单的相等谓词(选择)和标量运算符也更好。例如,属性compare-equal属性/常数在模型中通常可以很好地工作。修改直方图和频率统计信息以反映此类谓词的应用相对“容易”。
COALESCE
是基于构建的CASE
,而该构建又在内部实现IIF
(这IIF
在Transact-SQL语言中出现之前确实是正确的)。CE建模IIF
为UNION
具有两个互斥子级的子级,每个子级都包含一个有关输入关系选择的项目。列出的每个组件都具有模型支持,因此将它们组合起来相对简单。即便如此,抽象层越多,最终结果往往越不准确-这就是较大的执行计划趋于不稳定和可靠的原因。
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
树的组合(根据UNION
和IIF
记住)的组合以及对早期关键性冗余的多对多外部连接的关键信息,派生直方图和频率的影响,意味着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
,以及理想情况下对查询编译有用的外键和其他约束。
bigint
而不是,decimal(18, 0)
您将获得好处:1)使用8个字节而不是每个值使用9个字节,2)使用字节可比数据类型而不是打包数据类型,这可能会产生影响比较值时的CPU时间。