假如我们有一个分区表叫parent,里面只有一列数据,int类型的id。然后它是由三个分区表组成的--child1、child2和child3。他们分别是id从[0, 250),[250,500)以及[-10000,0)。
这里我child1和child2都加了索引,但是child3没有。所以是child3上面先进行一个排序,然后index扫描child1和child2,然后再用一个merge append节点来把它们的结果汇集起来。
但是不对啊。我们的分区表是分段分区的。child1里面所有的东西一定都是小于child2的。child3里面的东西一定都是小于其他两个分区的。本来里面的都是已经排好序了(index scan其实就是一种排序),为什么最后需要用merge append对已经排好序的结果再排一次呢?merge append本身是使用priority tree来实现的,还是有点开销的。这里我们应该可以用一个简单的append节点来代替。
2、让我们来修改内核吧首先我们要注意到这个问题是在排序的才会产生的。那么自然而然,我们就需要想办法在优化器中识别出这个状态。
什么样的情况可以增加我们想要的优化呢?我们要从一个分区表里面去排序地选择东西。这个分区表呢,是一个range partition table,而且它的每一个分区都没有子分区(因为那样实现起来太麻烦了)。
那么我们在优化器中该如何判断呢?首先应该想到的是PartitionBoundInfo这个结构体。它描述了分区表中每一个分区的上下限的关系,还有这个分区表的分区类型。
那么应该从哪儿下手呢?对源代码进行一点点搜索,你会发现这个函数:generate_mergeappend_paths。毕竟我们要把一个merge append的node变成一个append的node,从这个函数下手应该是合理的。
let's see some codes.
static voidgenerate_mergeappend_paths{ ListCell *lcp; bool is_range_partitioned = false; List *partition_keys = NIL; List *partition_keys_desc = NIL; PartitionBoundInfo bound_info = rel->boundinfo; if { is_range_partitioned = true; } if { partition_keys = generate_partition_pathkeys; partition_keys_desc = generate_partition_pathkeys; } foreach { List *pathkeys = lfirst; List *startup_subpaths = NIL; List *total_subpaths = NIL; bool startup_neq_total = false; ListCell *lcr; bool try_append = false; bool try_append_desc = false; if { if ) { try_append = true; } if ) { try_append_desc = true; } } foreach { RelOptInfo *childrel = lfirst; Path *cheapest_startup, *cheapest_total; cheapest_startup = get_cheapest_path_for_pathkeys; cheapest_total = get_cheapest_path_for_pathkeys; if { cheapest_startup = cheapest_total = childrel->cheapest_total_path; Assert; } if startup_neq_total = true; accumulate_append_subpath; accumulate_append_subpath; } //这里先不管desc的情况了。先让程序能够跑起来。 if { add_pathcreate_append_path); if { add_pathcreate_append_path); } } else { add_path create_merge_append_path); if add_path create_merge_append_path); } }}
这个代码实际上是我在原有的基础上改的,改动也不是很多。大家可以自己下载源代码来进行比对。
首先是这几个部分:
PartitionBoundInfo bound_info = rel->boundinfo; if { is_range_partitioned = true; } if { partition_keys = generate_partition_pathkeys; partition_keys_desc = generate_partition_pathkeys; }
这个是判断它是不是一个range partition table。如果是的话呢,就按照正反两个方向来生成两个需要对这个表排序的pathkey。在PostgreSQL中,pathkey是用来描述排序的变量。 generate_partition_pathkeys我是这样写的:
List *generate_partition_pathkeys{ PartitionScheme part_scheme = parent_rel->part_scheme; PartitionBoundInfo bound_info = parent_rel->boundinfo; if ) { // 我们只考虑range的情况。 //如果分区有默认分区的话,就需要merge append了。 return NIL; } List *result = NIL; for { PathKey *key; Expr *expr = linitial; key = make_pathkey_from_sortinfo, ScanDirectionIsBackward, 0, parent_rel->relids, false ); result = lappend; } return result;}
这个还是很好理解的。我们进入到了merge append了,不管怎么样我们总是要排序的。既然要排序,那就先把pathkey算出来吧。到时候可以用来做判断。
然后的程序就很简单了。我们对每一个subrelation去得到它的最佳的path。注意到这里的path有两个(对于每一个relation来说是cheapest_startup和cheapest_total) 。这个和最终优化器去算代价是有关系的。然后如果是我们需要的情况,那就用生成append path,然后加入到parent的pathlist中去。
我们的修改之旅到这里并没有结束。因为如果只是这样修改的话,那么结果会是这样的:
这里最上面的cost为0,主要是我太懒了没有更新cost计算的事情。
3、总结这篇文章主要是我读了:
这里的讨论而写的。周末花了一天的时间看了前三个版本的实现,然后花了一天来简单实现一下。当然,写这个最主要的原因是我勇者斗恶龙11打通关了,周末没事做。偶尔看看技术文章挺好的。