谓词下推分析(一)
本文基于pg13.8。
谓词下推阶段即是把约束条件下推对条件涉及的表上(RelOptlnfo),其中同时会涉及到等价类的推导,及建立逻辑连接关系(外连接的SpecialJoinInfo结构的创建与设置)。
-
等价类推导用来根据原有的等价约束推导出新的约束,并分发到对应的表上。
-
约束条件分为连接条件和过滤条件( WHERE后的为过滤条件, JOIN/ON 上的为连接条件)。
在query_planner 中的下面3个函数涉及谓词下推,等价推导。这次先对第一个函数进行分析,配合源码阅读更佳。
joinlist = deconstruct_jointree(root);
/*
* Reconsider any postponed outer-join quals now that we have built up
* equivalence classes. (This could result in further additions or
* mergings of classes.)
*/
reconsider_outer_join_clauses(root);
/*
* If we formed any equivalence classes, generate additional restriction
* clauses as appropriate. (Implied join clauses are formed on-the-fly
* later.)
*/
generate_base_implied_equalities(root);
一 deconstruct_jointree、deconstruct_recurse
首先是deconstruct_jointree
函数, deconstruct_jointree
函数会调用 deconstruct_recurse
函数递归扫描查询树中的表的关联树(root->parse->jointree),处理其中的约束条件,并把约束条件分发到合适的基表或关联表上(放入RelOptlnfo结构)。
在此过程中可能会把条件分发到下层的表上,即下推。此过程主要是对 jointree 中的3种节点类型进行处理, 对其中的每一个约束条件构造下推所需的信息,然后调用 distribute_qual_to_rels
函数传入约束及相关信息来下推约束到基表或关联表上。
root->parse->jointree 的结构中各节点关系如下, 以如下SQL 为例:
select * from
t1 left join
(
select t2.key1, t_3_4.key3 from
t2, (
select t3.key1, t4.key3 from
t3 left join t4 on t3.key1 = t4.key1 and t4.key2=2
where t4.key1 is null
) t_3_4
where t2.key1 = t_3_4.key1
) x on t1.key1 = x.key1 and x.key1> 1 ;
1. 函数参数及变量介绍
函数参数如下:
static List *
deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
Relids *qualscope, Relids *inner_join_rels,
List **postponed_qual_list);
入参:
- jtnode 当前处理节点
- below_outer_join 当前节点位于上层的外关联的 nullable side 时, 设置为true(deconstruct_jointree 中调用时为false)。如 a left join (b join c) , (b join c) 为 nullable side, b 就在 (b join c) 中。
出参:
- qualscope 当前节点下所有涉及的表的rtindex集合
- inner_join_rels 当前节点下 inner join 涉及的表的rtindex 集合
- postponed_qual_list 在当前节点之下不能下推的约束, 如
t1 left join LATERAL( t2 where t2.id=t1.id)
中 t2.id=t1.id 就不能在( t2 where t2.id=t1.id)
节点 下处理。 需要到t1 left join LATERAL( t2 where t2.id=t1.id)
处理。
返回值:
- joinlist 返回拉平的连接表
一些重要变量介绍:
- nonnullable_rels/nullable_rels 记录连接中 nonnullable-side 和nullable-side 的表
- below_outer_join 默认false, 为true时表示当前处理节点在out join 的nullable-side下。
2. 源码分析
在 deconstruct_recurse
函数中主要是获取所有的约束条件,并收集分发所需信息(qualscope,nonnullable_rels, ojscope,below_outer_join等),然后分发约束。对连接树中节点分成三类进行处理:
- RangeTblRef: 叶子节点,为具体的表, 用来收集分发信息 qualscope 和 joinlist
- qualscope:rtindex;
- inner_join_rels: null;
- joinlist:list_make1(jtnode)
- postponed_qual_list 不修改
- FromExpr :
- 遍历 fromlist 调用 deconstruct_recurse 处理每个节点,汇总每次调用的出参和返回值, 包括如下:
- *qualscope = bms_add_members(*qualscope, sub_qualscope);
- inner_join_rels 不汇总,只获取下层的结果
- joinlist list_concat(joinlist, sub_joinlist) or lappend(joinlist, sub_joinlist)
- child_postponed_quals 即下层返回的postponed_qual_list
- 如果 f->fromlist 长度大于1, 表示有多个表,表之间为inner join, 故设置 inner_join_rels 为 qualscope, 不然为从下层获取的inner_join_rels。
- 然后尝试处理下层不能处理的约束(child_postponed_quals),如果约束涉及的表都在当前层次之下(
bms_is_subset(pq->relids, *qualscope)
),则调用distribute_qual_to_rels 分发约束。 如果不能处理,返回给上层(放入 postponed_qual_list) - 遍历 quals 处理过滤条件(也即where 中的过滤条件) ,调用 distribute_qual_to_rels 下推过滤条件
- 遍历 fromlist 调用 deconstruct_recurse 处理每个节点,汇总每次调用的出参和返回值, 包括如下:
- JoinExpr : join on
- 根据 jointype 进行不同处理, 需要设置 nonnullable_rels, nullable_rels, qualscope, inner_join_rels。 在调用distribute_qual_to_rels 时传入。
- JOIN_INNER: 对左右节点分别调用 deconstruct_recurse。
*qualscope = bms_union(leftids, rightids)
- inner_join_rels = qualscope
- nonnullable_rels = NULL
- nullable_rels = NULL
- JOIN_LEFT/JOIN_ANTI: 对左右节点分别调用 deconstruct_recurse。 对右边调用时设置below_outer_join 为true
*qualscope = bms_union(leftids, rightids)
*inner_join_rels = bms_union(left_inners, right_inners);
- nonnullable_rels = leftids JOIN_ANTI 对于没匹配的会显示元组
- nullable_rels = rightids
- JOIN_SEMI: 对左右节点分别调用 deconstruct_recurse。
*qualscope = bms_union(leftids, rightids)
*inner_join_rels = bms_union(left_inners, right_inners);
- nonnullable_rels = NULL semijoin 对于不匹配的表不会显示
- nullable_rels = NULL semi join 不会有右表数据。
- JOIN_FULL: 对左右节点分别调用 deconstruct_recurse。
*qualscope = bms_union(leftids, rightids)
*inner_join_rels = bms_union(left_inners, right_inners);
- nonnullable_rels = *qualscope;
- nullable_rels = *qualscope
- JOIN_INNER: 对左右节点分别调用 deconstruct_recurse。
- 把 nullable_rels 添加到 root->nullable_baserels
- 处理 child_postponed_quals, 若能处理,则添加到 my_quals, 不能处理, 添加到 postponed_qual_list
my_quals = list_concat(my_quals, (List *) j->quals)
- 不为 inner join , 则调用 make_outerjoininfo 生成SpecialJoininfo sjinfo
- 遍历 my_quals, 调用 distribute_qual_to_rels 下推约束
- 把 sjinfo 加入 root->join_info_list, 并调用update_placeholder_eval_levels 调整 target evaluation levels for placeholders
- 计算返回的 joinlist
- JOIN_FULL: 不拉平, 有子集,
joinlist = list_make1(list_make2(leftjoinlist, rightjoinlist))
list[0]-> list_1 , list_1[0] -> list_1_1(leftjoinlist), list_1[1]->list_1_2 (rightjoinlist) - 若拉平后,不超出 join_collapse_limit 限制,则拉平
joinlist = list_concat(leftjoinlist, rightjoinlist)
- 其他情况, 也不能拉平,joinlist = list_make2(leftpart, rightpart), 如果 leftjoinlist/rightjoinlist 长度为1, 则leftpart/ rightpart 为list 中的元素,而不是list。
- JOIN_FULL: 不拉平, 有子集,
- 根据 jointype 进行不同处理, 需要设置 nonnullable_rels, nullable_rels, qualscope, inner_join_rels。 在调用distribute_qual_to_rels 时传入。
3 使用到的一些函数分析
3.1 make_outerjoininfo
为当前的外连接生成SpecialJoinInfo结构。
3.1.1 SpecialJoinInfo
SpecialJoinInfo结构如下:
struct SpecialJoinInfo
{
NodeTag type;
Relids min_lefthand; /* 用来限制连接顺序,LHS(Left-Hand-Side)的最小集 */
Relids min_righthand; /* 用来限制连接顺序,RHS(right-Hand-Side)的最小集 */
Relids syn_lefthand; /* SQL语法中的LHS */
Relids syn_righthand; /* SQL语法中的RHS */
JoinType jointype; /* always INNER, LEFT, FULL, SEMI, or ANTI */
bool lhs_strict; /* 连接条件对LHS是否严格, 用来限制连接顺序 */
bool delay_upper_joins; /* 不能和上层连接交换顺序,用来限制连接顺序can't commute with upper RHS */
/* Remaining fields are set only for JOIN_SEMI jointype: */
bool semi_can_btree; /* true if semi_operators are all btree */
bool semi_can_hash; /* true if semi_operators are all hash */
List *semi_operators; /* 连接条件中的操作符链表 OIDs of equality join operators */
List *semi_rhs_exprs; /* 连接条件中的右操作表达式链表 righthand-side expressions of these ops */
};
min_lefthand 和 min_righthand 表示建立某个连接关系最少需要的表,syn_lefthand 和 syn_righthand 是 sql 语法体现出来的表。如:
select * from (t1 left join t2 on t1.key1=t2.key1) left join t3 on t2.key2=t3.key2;
会创建两个SpecialJoinInfo.
第一个:
- min_lefthand: t1
- min_righthand: t2
- syn_lefthand: t1
- syn_righthand: t2
- lhs_strict: true t1.key1=t2.key1
- delay_upper_joins: false
第二个:
- min_lefthand: t2
- min_righthand: t3
- syn_lefthand: t1&& t2
- syn_righthand: t3
- lhs_strict: true t2.key2=t3.key2
- delay_upper_joins: false
新顺序t1 leftjoin (t2 leftjoin t3)
也符合上述的 min_lefthand/min_righthand/lhs_strict,即表示可以交换。这部分规则在pg源码的readme 中有描述,下面截取一部分:
(A leftjoin B on (Pab)) leftjoin C on (Pbc) = A leftjoin (B leftjoin C on (Pbc)) on (Pab)
predicate Pbc must fail for all-null B rows(that is, Pbc is strict for at least one column of B)
If Pbc is not strict, the first form might produce some rows with nonnull C columns where the second form would make those entries null.
3.1.2 函数参数及变量
函数头:
static SpecialJoinInfo *
make_outerjoininfo(PlannerInfo *root,
Relids left_rels, Relids right_rels,
Relids inner_join_rels,
JoinType jointype, List *clause);
参数:
- left_rels 外连接外表涉及的表的rtindex 集合
- right_rels 外连接内部涉及的表的rtindex 集合
- inner_join_rels 当前外连接下的所有内连接的rtindex的计划
- jointype 连接类型
- clause 关联条件
3.1.3 函数逻辑分析
- 遍历 root->parse->rowMarks, 处理FOR [KEY] UPDATE SHARE 作用于nullable-side 的情况,对于此情况报错。
- syn_lefthand/syn_righthand/jointype 直接设置为参数 left_rels/right_rels/jointype
- 调用compute_semijoin_info 设置semi join 相关变量, 这里不展开说明, 主要用于半连接的内部唯一化。
- 对于全连接,min_lefthand 和 min_righthand 设置为left_rels和right_rels,即与syn_lefthand/syn_righthand 相同
- 调用
pull_varnos
函数获取约束涉及的所有表的rtindex 集合。记录到clause_relids。 - 调用
find_nonnullable_rels
函数获取连接条件中的所有非 nonullable-side 的表, 记录到 strict_relids。 - 设置 lhs_strict,当strict_relids 与left_rels 有交集时,设置为true,表示连接条件对 LHS 严格。
- 初始化 min_lefthand 为约束条件中涉及到的 left_rels 中的表(
bms_intersect(clause_relids, left_rels)
),之后可能会添加其他表。 - 初始化 min_righthand 为约束条件中涉及到的right_rels中的表,与下层 innerjoin 涉及到的 right_rels 中的表(
bms_int_members(bms_union(clause_relids, inner_join_rels),right_rels)
)的集合,因为外连接中内表的下层 inner join 不能和上层外连接交换,故需要包含进min_righthand 中。 - 遍历下层外连接(root->join_info_list),判断是否能和下层外连接(是否为下层外连接通过判断外连接是否与left_rels/right_rels 有交集)交换顺序。分为三种情况。
- 下层为全外连接,不能交换顺序,把下层全外连接的涉及的表加入对应的集合min_lefthand/min_righthand中(看全外连接在LHS 还是RHS)。
- 下层为非全外连接(可能为semijoin, antijoin, leftjoin),出现在LHS, 若符合下述情况则不能交换顺序,把下层外连接涉及的表加入min_lefthand。
- 连接条件涉及下层外连接的RHS, 且当前外连接为semijoin 或者antijoin。
- 连接条件涉及下层外连接的RHS,当前外连接为leftjoin,且连接条件不严格。
- 下层为非全外连接(可能为semijoin, antijoin, leftjoin),出现在RHS,若符合下述情况(各情况通过|| 连接,即不符合上一个条件,才会判断下一个条件)则不能交换顺序,把下层外连接涉及的表加入min_righthand。
- 连接条件涉及下层外连接的RHS(a leftjoin (b leftjoin c) on Pac),涉及nullable-side,交换后语义不一致
- 连接条件不涉及下层外连接的min_lefthand
- 当前外连接为semijoin 或者antijoin
- 下层外连接为semijoin 或者antijoin
- 下层外连接的连接条件对它的LHS 不严格
- 下层外连接设置了delay_upper_joins
- 遍历root->placeholder_list, 对于作用于当前外连接nullable-side 的PHV, 需要保证 min_righthand 包含ph_eval_at。这部分涉及PlaceHolderVar 这边不展开说明, 主要是为了保证PlaceHolderVar 能正确的起效,需要扩展min_righthand。
- 经过上面处理,如果min_lefthand/min_righthand还是空(比如 a=1, min_righthand 为空),则直接设置为left_rels/right_rels。
二 distribute_qual_to_rels
分析约束,构造 RestrictInfo 节点并下推到具体的RelOptlnfo结构中(baserestrictinfo(基表) or joininfo list (连接表)中),同时如果约束是 mergejoinable operator 并且下层没有外连接,则放入等价类列表,用于生成等价类。如果当前条件在当前层次不能下推(涉及的表在当前层及一下不存在),则放入 postponed_qual_list, 尝试去上层进行下推。
1. 参数与变量介绍
函数声明:
static void
distribute_qual_to_rels(PlannerInfo *root, Node *clause,
bool is_deduced,
bool below_outer_join,
JoinType jointype,
Index security_level,
Relids qualscope,
Relids ojscope,
Relids outerjoin_nonnullable,
Relids deduced_nullable_relids,
List **postponed_qual_list)
只有 postponed_qual_list 为出参,返回不能处理的约束。
入参如下:
- clause 约束 ,单条件或析取范式(clause1 or clause2)
- is_deduced 是否为基于等价推理产生的新约束
- below_outer_join 是否处于外,反,半连接的 Nullable-side (上层),如: t1 left join (t2 join t3 on a=b) , a = b 就处于半连接的 Nullable-side
- jointype 连接类型, 非join on 的都为inner join
- security_level 安全相关,不分析
- qualscope 当前节点下所有涉及的表的relid 集合
- ojscope: 外,反连接:bms_union(sjinfo->min_lefthand,sjinfo->min_righthand);内连接,半连接: null
- outerjoin_nonnullable nonnullable-side 的集合, 非外连接为NULL
- deduced_nullable_relids 对于推理出的条件,通过这个变量来传递其所属的等价类所在的约束条件引用的下层连接的nullable-side 的表
下面对函数中涉及的一些重要变量进行说明:
-
bool is_pushed_down:false表示不能下推的连接条件, true 表示where 中的过滤条件或者能下推的连接条件
-
bool outerjoin_delayed:约束从当前层下推到下一层后,如果下层有外连接,过滤条件可能不能继续下推,对于这种情况设置为true。
-
bool maybe_equivalence: 能否生成等价类, true, 则表示可以
-
bool maybe_outer_join: true 表示为 outerjoin。在maybe_equivalence 为false,maybe_outer_join 为true的时候会生成单成员等价类,在reconsider_outer_join_ clauses 中会对这种等价类进行处理,尝试生成正式等价类。
-
Relids nullable_relids 记录约束引用的下层外连接中的nullable-side 的表, 没有则为null。
-
Relids relids 控制约束下发到哪里。经过下述逻辑处理,最后记录要下发的RelOptInfo 的relids。 初始为约束条件中涉及的表的rtindex集合。
-
RestrictInfo *restrictinfo , clause 转换后的结构,附带一些信息。下面介绍一下 RestrictInfo 中的一些成员变量。
typedef struct RestrictInfo { NodeTag type; Expr *clause; /* 对应的约束 */ bool is_pushed_down; /* true 表明是下推条件,下推后就是过滤条件, false表示在原来位置 */ bool outerjoin_delayed; /* true 表示不能继续下推到下层的outjoin 中 */ bool can_join; /* 可能被用于mergejoin or hashjoin */ bool pseudoconstant; /* true, 表示是一个常量表达式 */ /* 约束中引用的表的rtindex集合 */ Relids clause_relids; /* 应用这个约束需要的最小的表的rtindex集合,分发是用来确定分发到那一层 */ Relids required_relids; /* 如果这是outerjoin 的约束, 这表明nonnullable-side的表的rtindex集合,不然NULL*/ Relids outer_relids; /* 约束条件中涉及的下层外连接中的nullable-side 的表的rtindex集合*/ Relids nullable_relids; /* 如果是操作符表达式,用来记录两端的操作数对应的表 */ Relids left_relids; /* relids in left side of clause */ Relids right_relids; /* relids in right side of clause */ /* 多条件时使用,具体看下面的调用 make_restrictinfo 生成 RestrictInfo 节点逻辑 */ Expr *orclause; /* modified clause with RestrictInfos */ /* This field is NULL unless clause is potentially redundant: */ EquivalenceClass *parent_ec; /* generating EquivalenceClass */ /* 操作符族,适用于merge join, 若存在则可以尝试生成等价类 */ List *mergeopfamilies; /* opfamilies containing clause operator */ /* cache space for mergeclause processing; NULL if not yet set */ EquivalenceClass *left_ec; /* 左操作数对应等价类 */ EquivalenceClass *right_ec; /* 右操作数对应等价类 */ EquivalenceMember *left_em; /* 左操作数生成的等价类成员 */ EquivalenceMember *right_em; /* 右操作数生成的等价类成员 */ /* 约束条件是否适用于hashjoin, 适用则记录hashjoin操作符oid */ Oid hashjoinoperator; /* copy of clause operator */ } RestrictInfo;
2. 源码分析
-
通过 pull_varnos 函数提前记录约束条件中涉及的表,记录到relids 中。
-
如果relids不为qualscope的子集,即约束条件涉及的表不在当前级别下,则不能下推,需要到上层处理(比如使用了LATERAL,则约束条件可以为上层的表)。
-
如果 relids 为空, 即约束条件不涉及表,没有变量。pg 会进行特殊处理,分成4种情况进行处理:
- 外连接下: 约束留在原位, 比如
t1 left join t2 on 1=1
。 - 内连接,含有易失函数:易失函数只能在执行的时候求值,约束留在原位, 比如
t1 join t2 on 1 > random()
。 - 内连接,不含易失函数,below_outer_join 为true:约束留在原位, 比如
t1 left join (t2 join t3 on 1 = 1) on true
。 - 内连接,不含易失函数,below_outer_join 为false:上层没有外连接,则约束直接影响整条SQL,约束会提到最顶层。通过relids= get_relids_in_jointree获取所有基表relid,qualscope = bms_copy(relids)。比如
t1 inner join (t2 join t3 on 1 = 1) on true
- 外连接下: 约束留在原位, 比如
-
检查约束条件下推情况,设置相应变量:is_pushed_down,maybe_equivalence,maybe_outer_join,outerjoin_delayed,nullable_relids
-
处理推理出的约束,下推,无等价类:is_deduced 为true,即为推理出的约束,则可以下推, 不能生成等价类。
is_pushed_down = true; outerjoin_delayed = false; nullable_relids = deduced_nullable_relids; /* Don't feed it back for more deductions */ maybe_equivalence = false; maybe_outer_join = false;
-
处理不能下推的连接条件(外连接),不下推,尝试生成单成员等价类:约束涉及 nonnullable-side的表,则不能下推,下推会导致记录数变少(因为nonnullable-side的表可能因为下推的约束不满足导致被筛掉一部分数据,与未下推时不满足条件时补充null的外连接语义不符)。
is_pushed_down = false; maybe_equivalence = false; maybe_outer_join = true; /* 调用check_outerjoin_delay 搜集nullable_relids信息并设置relids。 */ outerjoin_delayed = check_outerjoin_delay(root, &relids, &nullable_relids, false); /* 调整relids 为对应的外连接涉及的relids,即分发到原位置 */ relids = ojscope;
-
处理过滤条件以及能下推的连接条件,下推约束 ,调用 check_outerjoin_delay 确定能不能生成等价类及继续下推到下层的outjoin 内(之后分析此函数实现):
is_pushed_down = true; /* 调用check_outerjoin_delay 检测约束是否会被下层的out join 阻塞(不能引用nullable-side的表 ), 如下sql 即会被阻塞: explain select * from t1 left join (select t2.key1 from t1 a left join t2 on true)x on x.key1 is null; QUERY PLAN ------------------------------------------------------------------------------ Nested Loop Left Join (cost=0.00..728447.70 rows=53060400 width=16) -> Seq Scan on t1 (cost=0.00..30.40 rows=2040 width=12) -> Materialize (cost=0.00..65227.33 rows=26010 width=4) -> Nested Loop Left Join (cost=0.00..65097.28 rows=26010 width=4) Filter: (t2.key1 IS NULL) -> Seq Scan on t1 a (cost=0.00..30.40 rows=2040 width=0) -> Materialize (cost=0.00..48.25 rows=2550 width=4) -> Seq Scan on t2 (cost=0.00..35.50 rows=2550 width=4) (8 rows) t2.key1 IS NULL只能下推到t1 a left join t2 的过滤条件,相当于t1 a left join t2 on xxx where t2.key1 IS NULL, 不能继续往下推到 t2上, 此时会在原先的relids(只有t2)中添加t1的relid */ outerjoin_delayed = check_outerjoin_delay(root, &relids, &nullable_relids, true); if (outerjoin_delayed) {/* 被阻塞,则不能生成等价类(单成员等价类也不行) */ maybe_equivalence = false; /* 调用check_redundant_nullability_qual 判断一种特殊情况 - 约束为 is null 且作用于antijoin 的右表,此情况下,约束可以直接消除(貌似没有SQL可以走到这。。。)*/ if (check_redundant_nullability_qual(root, clause)) return; } else { maybe_equivalence = true; /* outerjoin_nonnullable != NULL 表示当前约束为外连接的连接条件,此时当成约束位于外连接之下*/ if (outerjoin_nonnullable != NULL) below_outer_join = true; } /* 没必要尝试生成单成员等价类,因为没有引用nonnullable-side,具体看reconsider_outer_join_ clauses的分析 */ maybe_outer_join = false;
-
-
调用 make_restrictinfo 生成 RestrictInfo 节点,对应一个约束条件。 传入的 relids 为要分发到的表, 设置到required_relids。make_restrictinfo 逻辑如下:
-
约束条件为析取范式 ,即or连接的表达式,则递归调用make_sub_restrictinfos 生成 RestrictInfo节点, 如:(A or B or (C and (D or E))), 会生成如下结构 :
-
单条件,直接生成
-
-
从约束中提取涉及的列, 加到表的targetlist 中,表示需要获取表的这些字段。
-
调用
check_mergejoinable
函数检测约束是否为mergeJoinable, 是才可以生成等价类,同时获取对应的操作符族,设置mergeopfamilies,check_mergejoinable
判断标准如下:
-
不能为常量表达式 restrictinfo->pseudoconstant
-
必须是一个操作符表达式(OpExpr) is_opclause
-
只能有两个参数
-
操作符需要可用于merge join, 非array_eq和record_eq通过查询pg_operator 表中的oprcanmerge字段。array_eq 和 record_eq 获取操作类型的btree比较函数,需为 F_BTARRAYCMP ,F_BTRECORDCMP
-
不能包含可变函数
-
然后是生成等价类及分发约束相关逻辑,具体如下:
- mergejoinable 并且maybe_equivalence == true, 则可以生成等价类,此时再调用check_equivalence_delay(调用 check_outerjoin_delay 检测约束的left_relids 或 right_relids 是否会受到阻碍,如果阻碍则不能生成等价类)再次检测是否可以生成等价类, 可以则调用 process_equivalence 生成等价类,生成成功则直接返回, 此时不分发约束;check_equivalence_delay 失败或 process_equivalence 失败则在 mergeopfamilies 仍为true 的情况下, 调用 initialize_mergeclause_eclasses 生成单成员等价类,并分发约束。
- 如果 mergejoinable 并且maybe_equivalence == false ,同时 maybe_outer_join == true && restrictinfo->can_join , 即上面说的可以对于外连接尝试生成单成员等价类。 然后根据 nonullable-side 的表在约束条件的左边还是右边, 把约束放入 root->left_join_clauses(在左边) 或 root->right_join_clauses(在右边),对于full join, 放入 root->full_join_clauses。之后会在
reconsider_outer_join_clauses
函数中进行处理并分发约束。如果约束条件没有记录到left_join_clauses ,right_join_clauses 或 full_join_clauses, 则在此函数分发约束。 - 非 mergejoinable ,生成单成员等价类, 并分发约束。
- 对于生成等价类成功,或约束放入left/right/full_join_clauses的情况,都不会在此阶段分发约束,而是在之后的
reconsider_outer_join_clauses
与generate_base_implied_equalities
中分发。分发约束通过调用distribute_restrictinfo_to_rels
函数:根据 restrictinfo->required_relids 分发到对应的基表的 RelOptInfo::baserestrictinfo 或 连接表的RelOptInfo::joininfo。
下面介绍一下上面使用的一些重要函数:
3. 使用到的一些重要函数
3.1 check_outerjoin_delay
函数:
static bool
check_outerjoin_delay(PlannerInfo *root,
Relids *relids_p, /* in/out parameter */
Relids *nullable_relids_p, /* output parameter */
bool is_pushed_down);
部分参数介绍:
- relids_p 入/出参, 应用约束所需的最小表rtindex 集合, 在此函数中,在delay时可能被修改。在函数内实际使用relids(
relids = bms_copy(*relids_p)
) - nullable_relids_p 出参,记录约束涉及的下层外连接中的nullable-side 的表的rtindex 集合,也即阻碍下推的表
- is_pushed_down 入参,表示约束是否可以下推
此函数功能如下:
-
检测约束条件下推的过程中是否会受到下层外连接阻塞,导致不能下推到外连接中,不能完全下推。例子如下:
explain select * from t1 left join (select t2.key1 from t1 a left join t2 on true)x on x.key1 is null; QUERY PLAN ------------------------------------------------------------------------------ Nested Loop Left Join (cost=0.00..728447.70 rows=53060400 width=16) -> Seq Scan on t1 (cost=0.00..30.40 rows=2040 width=12) -> Materialize (cost=0.00..65227.33 rows=26010 width=4) -> Nested Loop Left Join (cost=0.00..65097.28 rows=26010 width=4) Filter: (t2.key1 IS NULL) -> Seq Scan on t1 a (cost=0.00..30.40 rows=2040 width=0) -> Materialize (cost=0.00..48.25 rows=2550 width=4) -> Seq Scan on t2 (cost=0.00..35.50 rows=2550 width=4) (8 rows) t2.key1 IS NULL只能下推到t1 a left join t2 的过滤条件,相当于t1 a left join t2 on xxx where t2.key1 IS NULL, 不能继续往下推到 t2上, 此时会在原先的relids(只有t2)中添加t1的relid
-
修改 relids_p 和 设置 nullable_relids_p
-
设置连接顺序不允许交换标志(SpecialJoinInfo::delay_upper_joins)
逻辑如下:
-
root->join_info_list 为空,即没有外连接,则
*nullable_relids_p = NULL
, 返回false,不阻塞。 -
不为空,则遍历每个记录的外连接, 判断约束条件是否引用了外连接的nullable-side(relids 与min_righthand是否有交集, join_full情况下判断与min_lefthand || min_righthand是否有交集), 如果引用了外连接的nullable-side,并且relids没有包含外连接的min_lefthand 或 min_righthand则需要阻塞。
/* 添加外连接的min_lefthand,min_righthand 到relids 中,这样分发时,不会往外连接下层分发 */ relids = bms_add_members(relids, sjinfo->min_lefthand); relids = bms_add_members(relids, sjinfo->min_righthand); /* 标记阻塞 */ outerjoin_delayed = true; /* 有阻塞,修改了relids, 需要再次遍历join_info_list */ found_some = true;
-
设置 nullable_relids:
bms_add_members(nullable_relids,sjinfo->min_righthand)
, 如果是全外联,则也添加sjinfo->min_lefthand
:bms_add_members(nullable_relids,sjinfo->min_lefthand)
-
处理一种特殊情况:
a leftjoin ( B leftjoin c ON Pbc WHERE Pc) ON Pab, Pc is not strict;
(如果Pc is strict,那么B leftjoin c 的外连接会被消除,变为inner join) , 如:select * from t1 left join (select t2.key1 from t1 a left join t2 on true where t2.key1 is null)x on true;
, 对于此情况,需要设置sjinfo->delay_upper_joins = true
, 用来表示下层外连接(t1 a left join t2
)不能与上层(t1 left join (xxx)
)交换连接顺序。
3.2 check_equivalence_delay
会对约束的left_relids 或 right_relids 调用 check_outerjoin_delay
检测是否会受到阻碍,其实是检测在以left_relids 或right_relids 作为推出的新约束的一部分的情况下,新约束的下推是否能完全下推,还是会被外连接阻塞。
如果新约束被阻塞,则不能用于生成等价类,因为根据等价类生成的新约束会直接下发到对应的表中,不会检查能否下发。例子如下:
select * from t1 left join t2 on true where coalesce(t2.key1, 10) = t1.key1 and coalesce(t2.key1, 20) = t1.key1
, 此 SQL 由于使用了 coalesce,导致两个约束条件是不严格的(严格表示为:输入NULL, 输出为NULL 或false。 此处 t2.key1 为null 时,表达式可能为TRUE),外连接不能消除。
在distribute_qual_to_rels 中处理逻辑如下:
- 因为是where中的条件, 会进入处理过滤条件以及能下推的连接条件步骤,然后调用
check_outerjoin_delay
检测。 - 检测时虽然符合
bms_overlap(relids, sjinfo->min_righthand)
但不符合!bms_is_subset(sjinfo->min_lefthand, relids) || !bms_is_subset(sjinfo->min_righthand, relids)
导致约束条件没有被阻塞(因为约束不会下推到t1或t2 上,不需阻塞)。 - 此时会发现可以尝试生成等价类,如果生成了等价类,会推导出
coalesce(t2.key1, 10) = coalesce(t2.key1, 20)
的新约束条件,而这个新约束条件会分发到t2 表上,导致SQL语义改变,实际上不能分发。因此需要此函数来处理此种情况。
3.3 process_equivalence
函数:
bool
process_equivalence(PlannerInfo *root,
RestrictInfo **p_restrictinfo,
bool below_outer_join)
参数:
- p_restrictinfo 用于生成等价类的约束
- below_outer_join true 表示当前约束条件在上层的外关联的 nullable side
此函数用来生成等价类,生成成功返回true。主要处理逻辑如下:
-
对形如 X=X 的约束条件,不生成等价类,如果约束是严格的,则把约束转换为 X IS NOT NULL 形式的约束。
-
约束的两边如果有包含非严格函数,也不生成等价类。只有在约束的两边为常量,或使用的函数为严格函数时,才能生成等价类。
-
对约束的两边在现有的等价类(root->eq_classes)中查找是否有匹配的等价类(EquivalenceClass)和等价成员(EquivalenceMember)
-
之后生成等价类,
-
如果左右两边的表达式都匹配到等价类, 且匹配的是同一个。只是简单的把这个约束加到这个等价类中。
-
如果左右两边的表达式都匹配到等价类, 匹配的不是同一个, 合并等价类,并加入这个约束条件。
-
如果只有左边匹配到,则把右边的表达式加到左端匹配的等价类中(加到EquivalenceClass::ec_members(EquivalenceMember) ), 并加入这个约束条件 。
-
如果只有右边匹配到,则把左边的表达式加到右端匹配的等价类中,并加入这个约束条件。
-
都没有匹配,则创建一个新的等价类。
-
加入约束,是加入到
EquivalenceClass::ec_sources
中,并设置约束中的等价类相关信息/* mark the RI as associated with this eclass */ restrictinfo->left_ec = ec1; // 或者ec2, 看实际匹配的等价类 restrictinfo->right_ec = ec1; // 或者ec2, 看实际匹配的等价类 /* mark the RI as usable with this pair of EMs */ restrictinfo->left_em = em1; restrictinfo->right_em = em2;
-
3.4 单成员等价类
单成员等价类,可用于在之后对这种等价类进行合并;还可以用来构建 PathKeys。
) ), 并加入这个约束条件 。
-
如果只有右边匹配到,则把左边的表达式加到右端匹配的等价类中,并加入这个约束条件。
-
都没有匹配,则创建一个新的等价类。
-
加入约束,是加入到
EquivalenceClass::ec_sources
中,并设置约束中的等价类相关信息/* mark the RI as associated with this eclass */ restrictinfo->left_ec = ec1; // 或者ec2, 看实际匹配的等价类 restrictinfo->right_ec = ec1; // 或者ec2, 看实际匹配的等价类 /* mark the RI as usable with this pair of EMs */ restrictinfo->left_em = em1; restrictinfo->right_em = em2;
3.4 单成员等价类
单成员等价类,可用于在之后对这种等价类进行合并;还可以用来构建 PathKeys。