合并连接处理按连接键排序的数据集,并返回以类似方式排序的结果。输入集可以在索引扫描后预先排序;否则,执行者必须在实际合并开始之前对它们进行排序。
归并排序集
让我们看一个合并连接的例子;它在执行计划中由Merge Join节点表示:
优化器更喜欢这种连接方法,因为它返回排序的结果,如ORDER BY子句定义的那样。在选择计划时,优化器会注意数据集的排序顺序,除非确实需要,否则不会执行任何排序。例如,如果合并连接产生的数据集已经具有适当的排序顺序,则可以在随后的合并连接中使用:
首先要连接的表是机票和登机牌;它们都有一个复合主键(ticket_no,flight_id),结果按这两列排序。然后将生成的行集与票券表连接,票券表按ticket_no列排序。
连接只需要对两个数据集进行一次传递,并且不占用任何额外的内存。它使用两个指针指向内部集和外部集的当前行(最初是第一行)。
如果当前行的键不匹配,则其中一个指针(引用具有较小键的行)将向前移动到下一行,直到找到匹配。连接的行返回到上面的节点,并且内部集合的指针向前移动一位。操作将继续,直到其中一个集合结束。
该算法处理内部集合的副本,但外部集合也可以包含它们。因此,必须改进算法:如果在外部指针前进后键保持不变,则内部指针返回到第一个匹配行。因此,外部集合的每一行都将与具有相同键的内部集合的所有行相匹配。
对于外部连接,算法进一步调整了一点,但它仍然基于相同的原理。
合并连接条件只能使用相等操作符,这意味着只支持相等连接(尽管对其他条件类型的支持目前也在进行中)。
成本预估。 进一步查看刚刚的例子:
连接的启动成本至少包括所有子节点的启动成本。
通常,在找到第一个匹配之前,可能需要扫描外部或内部集合的一部分。可以通过比较(基于直方图)两个集合中最小的连接键来估计这个比例。但在这种特殊情况下,两个表中的票号范围是相同的。
总成本包括从子节点获取数据的成本和计算成本。
因为一旦其中一个集合结束,连接算法就会停止(当然,除非执行外部连接),所以可能只会部分扫描另一个集合。为了估计被扫描部件的尺寸,我们可以比较两个集合中的最大键值。在本例中,两个集合都将被完整读取,因此连接的总成本包括两个子节点的总成本之和。
此外,如果存在任何重复,则内部集合的某些行可能被扫描多次。估计的重复扫描次数等于连接结果的基数与内部集合的基数之差。在这个查询中,这些基数是相同的,这意味着集合不包含重复项。
该算法比较两个集合的连接键。一次比较的代价是在cpu_operator_cost值上估计的,而估计的比较次数可以看作是两个集合的行数之和(由重复引起的重复读取次数增加)。像往常一样,结果中包含的每一行的处理成本都是在cpu_tuple_cost值上估计的。
因此,在这个例子中,连接的成本估计如下:
并行模式
虽然合并连接没有并行特性,但它仍然可以在并行计划中使用。
外部集合可以由多个工作进程并行扫描,但内部集合总是由每个工作进程全部扫描。
由于并行散列连接几乎总是更便宜,我将关闭它一段时间:
下面是一个使用合并连接的并行计划的例子:
在并行计划中不允许完全和右外部合并连接。
修改
合并连接算法可用于任何类型的连接。唯一的限制是完整和右外部连接的连接条件必须包含合并兼容的表达式(“外列等于内列”或“列等于常量”)。内部连接和左外部连接只是根据不相关的条件过滤连接结果,但对于完全连接和右连接,这种过滤是不适用的。
下面是一个使用合并算法的全连接示例:
内部和左合并连接保留排序顺序。但是,完全外部连接和右外部连接不能保证这一点,因为NULL值可能会插入外部集的有序值之间,从而破坏排序顺序。为了恢复所需的顺序,规划器在这里引入Sort节点。自然地,它增加了计划的成本,使散列连接更有吸引力,所以计划器选择这个计划只是因为当前禁用了散列连接。
但是下一个示例不能没有散列连接:嵌套循环根本不允许完全连接,而由于不支持连接条件而不能使用合并。因此,无论enable_hashjoin参数值如何,都将使用散列连接:
让我们恢复之前禁用的哈希连接功能: