我有两个具有相同名称、类型和索引键列的表。其中一个具有唯一的聚集索引,另一个具有非唯一的.
测试设置
设置脚本,包括一些真实的统计数据:
DROP TABLE IF EXISTS #left;
DROP TABLE IF EXISTS #right;
CREATE TABLE #left (
a char(4) NOT NULL,
b char(2) NOT NULL,
c varchar(13) NOT NULL,
d bit NOT NULL,
e char(4) NOT NULL,
f char(25) NULL,
g char(25) NOT NULL,
h char(25) NULL
--- and a few other columns
);
CREATE UNIQUE CLUSTERED INDEX IX ON #left (a, b, c, d, e, f, g, h)
UPDATE STATISTICS #left WITH ROWCOUNT=63800000, PAGECOUNT=186000;
CREATE TABLE #right (
a char(4) NOT NULL,
b char(2) NOT NULL,
c varchar(13) NOT NULL,
d bit NOT NULL,
e char(4) NOT NULL,
f char(25) NULL,
g char(25) NOT NULL,
h char(25) NULL
--- and a few other columns
);
CREATE CLUSTERED INDEX IX ON #right (a, b, c, d, e, f, g, h)
UPDATE STATISTICS #right WITH ROWCOUNT=55700000, PAGECOUNT=128000;
复制品
当我在它们的集群键上加入这两个表时,我期望一个一对多的 MERGE 连接,如下所示:
SELECT *
FROM #left AS l
LEFT JOIN #right AS r ON
l.a=r.a AND
l.b=r.b AND
l.c=r.c AND
l.d=r.d AND
l.e=r.e AND
l.f=r.f AND
l.g=r.g AND
l.h=r.h
WHERE l.a='2018';
这是我想要的查询计划:
(不要介意警告,它们与虚假统计有关。)
但是,如果我更改连接中列的顺序,如下所示:
SELECT *
FROM #left AS l
LEFT JOIN #right AS r ON
l.c=r.c AND -- used to be third
l.a=r.a AND -- used to be first
l.b=r.b AND -- used to be second
l.d=r.d AND
l.e=r.e AND
l.f=r.f AND
l.g=r.g AND
l.h=r.h
WHERE l.a='2018';
... 有时候是这样的:
Sort 运算符似乎根据声明的连接顺序对流进行排序,即c, a, b, d, e, f, g, h
,它向我的查询计划添加了阻塞操作。
我看过的东西
- 我尝试将列更改为
NOT NULL
,结果相同。 - 原始表是用 创建的
ANSI_PADDING OFF
,但创建它ANSI_PADDING ON
不会影响这个计划。 - 我尝试了一个
INNER JOIN
而不是LEFT JOIN
,没有变化。 - 我在 2014 SP2 Enterprise 上发现了它,在 2017 Developer(当前 CU)上创建了一个复制品。
- 删除前导索引列上的 WHERE 子句确实会产生好的计划,但它会影响结果.. :)
最后,我们进入问题
- 这是故意的吗?
- 我可以在不更改查询的情况下消除排序吗(这是供应商代码,所以我真的不想......)。我可以更改表和索引。
这是设计使然,是的。不幸的是,当 Microsoft 停用 Connect 反馈站点时,该断言的最佳公共资源丢失了,删除了 SQL Server 团队开发人员的许多有用评论。
无论如何,当前的优化器设计并没有主动寻求避免不必要的排序本身。这在窗口函数等中最常遇到,但也可以在其他对排序敏感的运算符中看到,特别是对运算符之间的保留顺序敏感。
尽管如此,优化器在避免不必要的排序方面非常好(在许多情况下),但这种结果通常是由于积极尝试不同的排序组合之外的原因而发生的。从这个意义上说,与其说是“搜索空间”的问题,不如说是正交优化器功能之间的复杂交互,这些交互已被证明可以以可接受的成本提高总体规划质量。
例如,通常可以简单地通过将排序要求(例如顶级
ORDER BY
)与现有索引匹配来避免排序。在您的情况下微不足道,这可能意味着添加ORDER BY l.a, l.b, l.c, l.d, l.e, l.f, l.g, l.h;
,但这是一种过度简化(并且不可接受,因为您不想更改查询)。更一般地,每个备忘录组可以与所需的或期望的属性相关联,这些属性可以包括输入排序。当没有明显的理由强制执行特定命令时(例如,为了满足
ORDER BY
,或确保来自对命令敏感的物理运算符的正确结果),则涉及“运气”元素。我写了更多关于它的细节,因为它与合并连接(在联合或连接模式下)有关,在使用 Merge Join Concatenation 避免排序。其中大部分超出了产品支持的表面积,因此请将其视为信息,并且可能会发生变化。在您的特定情况下,是的,您可以按照 jadarnel27的建议调整索引以避免排序;尽管没有什么理由更喜欢这里的合并连接。您还可以在不更改查询的情况下使用计划指南提示在散列或循环物理连接之间进行选择
OPTION(HASH JOIN, LOOP JOIN)
,具体取决于您对数据的了解,以及最佳、最差和平均情况性能之间的权衡。最后,出于好奇,请注意,可以使用简单的 来避免排序
ORDER BY l.b
,但代价是可能效率较低的多对多合并连接b
,并且具有复杂的残差。我提到这主要是为了说明我之前提到的优化器功能之间的交互,以及顶级需求可以传播的方式。如果您可以更改索引,则更改索引的顺序
#right
以匹配连接中过滤器的顺序会删除排序(对我而言):令人惊讶的是(至少对我而言),这导致两个查询都没有以排序结束。
查看一些奇怪的跟踪标志的输出,最终的备忘录结构有一个有趣的区别:
正如您在顶部的“根组”中看到的那样,两个查询都可以选择使用 Merge Join 作为主要物理操作来执行此查询。
很好的查询
没有排序的连接由组 29 选项 1 和组 31 选项 1 驱动(每个选项都是对所涉及索引的范围扫描)。它由组 27(未显示)过滤,这是过滤连接的一系列逻辑比较操作。
错误查询
具有排序的那个是由这两组(29 和 31)中的每一个都具有的(新)选项 3 驱动的。选项 3 对前面提到的范围扫描的结果执行物理排序(每个组的选项 1)。
为什么?
出于某种原因,在第二个查询中,优化器甚至无法直接使用 29.1 和 31.1 作为合并连接的源。否则,我认为它将在其他选项中列在根组下。如果它完全可用,那么它肯定会选择那些更昂贵的排序操作。
我只能得出以下结论:
希望有人能过来解释为什么需要排序,但我认为备忘录建筑的差异很有趣,可以作为答案发布。