概述
在查询优化过程中,Oceanbase使用动态规划算法自下向上地对join order进行枚举,然后使用冲突规则对枚举的合法性进行判断,能够在保证正确性的基础上进行充分的探索,为得到最优的执行计划提供了基础。
基本原理
Oceanbase中join order的生成过程分为两个主要的阶段:
冲突检测器生成:递归的遍历查询语句中存在的连接关系,为其创建对应的冲突检测器和冲突规则。
join order枚举:使用动态规划算法,自下向上地进行枚举遍历。
考虑如下情况:
SELECT * FROM
(t1 LEFT JOIN t2 ON t1.c1 = t2.c1),
t3
WHERE t1.c2 = t3.c2
对于上述查询,可以生成如下冲突检测器:
外连接冲突检测器:table_set_:<t1, t2>, L_DS_:<t1>, R_DS_:<t2>, L_TES_:<t1>, R_TES_:<t2>
内连接冲突检测器:table_set_:<t1, t3>, L_DS_:<t1, t2, t3>, R_DS_:<t1, t2, t3>, L_TES_:<t1, t3>, R_TES_:<t1, t3>, CR_: <[t2] -> [t2], [t2] -> [t1]>
交叉连接冲突检测器:table_set_:<t1, t3>, L_DS_:<t1, t2, t3>, R_DS_:<t1, t2, t3>, cross_product_rule_:<[t1, t2] ->[t1, t2]>, delay_cross_product_rule_:<[t1, t2, t3] ->[t1, t2, t3]>
利用生成的冲突检测器,使用动态规划算法,可以形成如下枚举过程:
最终生成的合法连接路径为:
(t1 left join t2) inner join t3
(t1 left join t3) inner join t2
代码解析
join order生成逻辑的入口为ObLogPlan::generate_join_orders,执行流程如下:
调用get_base_table_items函数获取查询语句中涉及的基表,包含from表和semi信息中的表。
调用generate_base_level_join_order函数,为各基表创建ObJoinOrder节点。
调用pre_process_quals函数对where条件、连接条件和半连接条件中的谓词进行预处理。
调用generate_conflict_detectors函数为查询语句中涉及的连接操作生成冲突检测器,执行流程如下:
调用generate_inner_join_detectors函数为from表之间形成的内连接创建冲突检测器。
调用generate_semi_join_detectors函数为半连接创建冲突检测器。
创建高度为基表数的ObJoinOrder节点树(下称join_rels),然后将第0层设置为基表的ObJoinOrder节点并调用其generate_base_paths函数生成对应的基表访问路径。
调用init_leading_info函数根据查询语句中的hint初始化leading信息,执行流程如下:
如果查询语句中使用了ORDERED hint,则调用init_leading_info_from_tables函数,按照from表的顺序初始化leading信息。
如果查询语句中使用了LEADING hint,则调用init_leading_info_from_leading函数,按照hint中指定的表初始化leading信息。
调用init_bushy_tree_info函数收集leading信息和join表中的bushy tree的子表集合,放到bushy_tree_infos_中,后者的主要作用是在join order的枚举过程中作为early stop的判断条件。
自下向上的生成join order,如果查询语句涉及的基表小于10或leading信息不为空,则调用generate_join_levels_with_DP函数进行生成,否则调用generate_join_levels_with_linear函数进行生成。
生成冲突检测器
generate_inner_join_detectors函数负责为存在内连接关系的表创建冲突检测器,执行流程如下:
遍历表集合(下称table_items),从谓词集合中提取出属于当前表的谓词,然后调用generate_outer_join_detectors函数为当前表中可能存在的连接操作生成冲突检测器(下称outer_join_detectors)。
遍历谓词集合中的连接条件(即非单表条件),使用条件中的表集合检查是否能够在冲突检测器集合中(下称inner_join_detectors)找到对应的冲突检测器,如果无法找到则需要创建新的冲突检测器。创建完成后,新的冲突检测器会被添加到inner_join_detectors中,并按照如下方式设置其中的参数:
将当前谓词中的表集合添加到join_info_.table_set_中,将join_info_.join_type_设置为内连接。
将is_degenerate_pred_设置为false(当前谓词为非单表条件)。
将is_commutative_设置为内连接对应的属性。
将当前谓词添加到join_info_.where_condition_中。
如果当前谓词为左右两表的equal条件,则将其添加到join_info_.equal_join_condition_中。
如果能够找到对应的冲突检测器,则只需要对其执行最后两步即可。
遍历inner_join_detectors,为其中的冲突检测器设置参数和冲突规则,执行流程如下:
遍历table_items中的表,将其中与当前冲突检测器的join_info_.table_set_存在交集的表(及内连接的子表)添加到L_DS_和R_DS_中。
将当前冲突检测器的join_info_.table_set中的表添加到L_TES_和R_TES_中。
遍历outer_join_detectors,对其中外连接的冲突检测器,将其分别作为当前冲突检测器的左右子冲突检测器,调用generate_conflict_rule函数生成冲突规则存储到当前检测器的CR_中。
将outer_join_detectors合并到inner_join_detectors中,然后调用generate_cross_product_detector函数创建交叉连接的冲突检测器。
generate_cross_product_detector函数会为内连接的子表创建冲突检测器,然后按照如下方式设置冲突检测器中的参数:
将table_items中的表添加到L_DS_和R_DS_中。
调用generate_cross_product_conflict_rule函数生成当前冲突检测器的冲突规则,执行流程如下:
对于table_items中的每一项,使用其子表id创建冲突规则,添加到cross_product_rule_中。
这里举例进行说明,考虑如下查询:
select * from (t1 inner join t2 on t1.c1 = t2.c1), t3
根据冲突规则,满足条件的交叉连接应该为:左表<t1 inner join t2>,右表<t3>
使用连接谓词对table_items进行划分,将存在连接条件的项划分到同一子集中。对于各子集,使用其子表id创建冲突检测规则,添加到delay_cross_product_rule_中(这项规则应该是性能上的考虑,因为交叉连接会生成较大的中间结果,所以应该先执行内连接)。
这里举例进行说明,考虑如下查询:
select * from t1, t2, t3, t4 where t1.c1 = t2.c1 and t3.c1 = t4.c1
根据冲突规则,满足条件的交叉连接应该为:左表<t1 inner join t2>,右表<t3 inner join t4>
对于上一步划分得到的子集,将其中大于1的子集构成的子集对的子表集合添加到bushy_tree_infos_中(上例中对应的子表集合为<t1, t2, t3, t4>)。
将join_info_.join_type_设置为内连接(交叉连接可以看作退化的内连接)。
将is_degenerate_pred_设置为false(交叉连接没有连接条件)。
将is_commutative_设置为true。
generate_outer_join_detectors函数负责为内连接的子表中可能存在的连接操作生成冲突检测器,执行流程如下:
如果当前表为非join表,则将谓词下推到子表对应的ObJoinOrder中。
如果当前表为join表,且连接方式为内连接,则调用generate_inner_join_detectors函数为其生成冲突检测器。
如果当前表为join表,且连接方式为外连接,则调用inner_generate_outer_join_detectors函数为其生成冲突检测器。
inner_generate_outer_join_detectors函数负责为存在外连接关系的表创建冲突检测器,执行流程如下:
调用pushdown_where_filters函数从当前表的where条件中取出左右子表的单表条件,并从where条件中移除。
调用pushdown_on_conditions函数从连接条件中取出左右子表的单表条件,并从连接条件中移除。
为左右子表分别调用generate_outer_join_detectors函数生成冲突检测器(下称left_detectors和right_detectors),并添加到返回结果中。
为当前外连接创建冲突检测器,并按照如下方式设置冲突检测器中的参数:
将连接条件涉及到的表集合添加到join_info_.table_set_中。
将外连接的左右子表分别添加到L_DS_和R_DS_中。
将外连接的连接条件添加到join_info_.on_condition_中。
将join_info_.join_type_设置为对应的外连接类型,将is_commutative_设置为外连接类型对应的属性。
如果连接条件涉及到的表集合与左右表都存在交集,则将is_degenerate_pred_设置为false,否则设置为true。
将L_TES_设置为join_info_.table_set_与L_DS_的交集,R_TES_设置为join_info_.table_set_与R_DS_的交集。
如果连接条件为左右两表的equal条件,则将其添加到join_info_.equal_join_condition_中。
分别遍历left_detectors和right_detectors,将其中的冲突检测器分别作为当前冲突检测器的子冲突检测器,调用generate_conflict_rule函数生成冲突规则存储到当前检测器的CR_中。
如果where条件不为空,则需要为其创建内连接的冲突检测器(因为存在空值拒绝条件),并按照如下方式设置其中的参数:
将where条件中的谓词添加到join_info_.where_condition_中。
将外连接的左右表添加到join_info_.table_set_中。
将is_degenerate_pred_设置为false(单表条件已移除)。
将join_info_.join_type_设置为内连接,将is_commutative_设置为内连接对应的属性。
generate_semi_join_detectors函数会遍历查询语句的semi信息,然后为其创建对应的冲突检测器,并按照如下方式设置其中的参数:
将半连接的左右表分别添加到L_DS_和R_DS_中。
将join_info_.join_type_设置为对应的连接类型,将is_commutative_设置为连接类型对应的属性。
将连接条件涉及到的表集合添加到join_info_.table_set_中。
将连接条件添加到join_info_.where_condition_中。
如果连接条件为左右两表的equal条件,则将其添加到join_info_.equal_join_condition_中。
将L_TES_设置为join_info_.table_set_与L_DS_的交集,R_TES_设置为join_info_.table_set_与R_DS_的交集。
如果连接条件涉及到的表集合与左右表都存在交集,则将is_degenerate_pred_设置为false,否则设置为true。
遍历内连接冲突检测器集合,将其中的冲突检测器作为当前冲突检测器的父冲突检测器,调用generate_conflict_rule函数生成冲突规则存储到当前检测器的CR_中。
generate_conflict_rule函数负责为冲突检测器创建冲突规则,执行逻辑如下:
如果子检测器位于父检测器的左侧:
调用satisfy_associativity_rule函数判断子检测器与父检测器的连接关系是否满足结合律,如果不满足则按照如下流程添加冲突规则:
获取子检测器的L_DS_和join_info_.table_set_的交集(下称ids)。
如果ids为空,则分别调用add_conflict_rule函数添加R_DS_-> L_DS_和R_DS_-> R_DS_规则。
如果ids不为空,则分别调用add_conflict_rule函数添加R_DS_-> ids和R_DS_-> R_DS_规则。
调用satisfy_left_asscom_rule函数判断子检测器与父检测器的连接关系是否满足左交换结合律,如果不满足则按照如下流程添加冲突规则:
获取子检测器的R_DS_和join_info_.table_set_的交集(下称ids)。
如果ids为空,则调用add_conflict_rule函数添加L_DS_-> R_DS_规则。
如果ids不为空,则调用add_conflict_rule函数添加L_DS_-> ids规则。
如果子检测器位于父检测器的右侧,则按照对称的逻辑添加冲突规则,这里不再赘述。
join order生成
generate_join_levels_with_DP函数会调用inner_generate_join_levels_with_DP函数按照动态规划的方式,自下向上地进行join order枚举。该函数首先会尝试使用leading信息进行枚举,如果无法找到符合要求的join order,则会忽视leading信息重新执行。
inner_generate_join_levels_with_DP函数会使用循环自下向上地对join_rels中的对应层级进行填充,当前需要处理的每一层节点都由下层节点通过join得到,因此在每层节点的处理过程中,都需要遍历不同的下层节点组合(如第3层可以有<2, 0>,<1, 1>两种组合),然后对于每种组合按照如下流程生成当前层的节点:
根据当前下层节点的组合方式,调用generate_single_join_level_with_DP函数对当前层进行生成和填充。
调用check_need_bushy_tree函数判断是否需要遍历下一种组合方式。如果当前层不为空,且bushy_tree_infos_中属于当前层的项(即表集合的大小等于当前层数加1的项)都能够在当前层中找到子表集合与其相同的节点,则可以结束当前层的处理(这里相当于满足了early stop条件,考虑上面3层的例子,如果<2, 0>组合能够满足要求则无需尝试<1, 1>组合,从而避免生成bushy tree)。
generate_single_join_level_with_DP函数会遍历左右层的ObJoinOrder节点对,然后按照如下流程尝试为其生成当前层的join order:
如果需要使用leading信息,则需要调用check_join_hint函数检查当前ObJoinOrder节点对中的左右表组合是否与leading信息匹配,判断逻辑如下:
如果左右子表集合都与leading表没有交集,则说明当前组合不涉及leading信息。
如果左右子表集合都是leading表的子集,则进一步查找leading信息中是否存在满足条件的项,如果存在则说明当前组合与leading信息相符,否则说明当前组合与leading信息不符。
如果leading表是左表的子集,则说明leading信息在左表的生成过程中已经得到处理,因而无需进一步关注。
如果上述条件都不满足,说明当前组合与leading信息不符。
调用inner_generate_join_order函数尝试为当前ObJoinOrder节点对生成连接后的节点,填充到join_rels中的指定层中。如果当前组合与hint匹配,则需要放松delay_cross_product条件(hint优先)。
如果当前组合无法找到合法的连接方式,且当前组合与leading信息相符,同时leading表不是左表的子集,说明leading信息不可用,需要退出处理流程以便忽视hint重试。
generate_join_levels_with_linear函数会按照线性处理的方式自下向上地地进行join order枚举,执行流程如下:
调用preprocess_for_linear函数对join_rels进行预处理,即按照查询语句中join表的连接方式对join_rels进行预填充。
调用inner_generate_join_levels_with_linear函数使用循环自下而上的对join_rels中的对应层级进行填充,该函数会调用generate_single_join_level_with_linear函数对当前层进行生成和填充。
generate_single_join_level_with_linear函数首先会将当前左层的节点按照预估的输出行数从小到大进行排序,然后遍历左右层的ObJoinOrder节点对,调用inner_generate_join_order函数尝试为当前节点对生成连接后的节点,填充到join_rels中的指定层中。外层节点遍历过程中,如果发现当前需要填充的层已经不为空,则退出遍历。如果上述遍历过程执行完成后,当前需要填充的层仍然为空,则尝试放松delay_cross_product条件再进行一次遍历。由于在预处理阶段已经使用join表的连接方式对join_rels进行了预填充,因此在枚举阶段实际上只需要处理from表之间的内联接(包括交叉连接),且外层节点的循环条件中添加了前述提前退出条件,因此大大减少了实际需要枚举判断的组合数,然而另一方面也限制了优化空间。
inner_generate_join_order函数负责为当前ObJoinOrder节点对生成连接后的节点。该函数首先会检查左右子表集合是否不存在交集,然后调用choose_join_info函数尝试为当前ObJoinOrder节点对找到匹配的冲突检测器。如果无法找到,说明对于当前节点对不存在合法的连接关系,否则使用找到的冲突检测器进行进一步处理。如果在join_rels的当前层中能找到与当前连接后的子表集合匹配的ObJoinOrder节点,则不需要创建新的节点,否则需要创建新的节点并调用ObJoinOrder::init_join_order函数对其进行初始化,然后将其添加到join_rels中。对于找到的节点,如果当前处理的是join_rels的第1层且当前调用不涉及leading信息中的顺序要求,则无需进一步生成连接路径(考虑a、b两表,如果已经为ab生成了连接路径,则无需为ba生成连接路径,因为在生成阶段会同时生成正反连接路径),否则需要按照如下流程对其进行进一步处理:
调用ObJoinOrder::merge_conflict_detectors函数将匹配的冲突检测器合并到当前节点中,在父节点生成时会忽略该冲突检测器。
调用ObJoinOrder::generate_join_paths函数为当前节点对生成连接路径。
choose_join_info函数会遍历冲突检测器集合,然后调用check_join_legal函数判断当前节点对与冲突检测器是否匹配,后者执行流程如下:
如果连接方式为内连接,则检查join_info_.table_set_是否包含于左右子表集合的并集;否则需要检查L_TES_和R_TES_是否分别包含于左右子表集合,如果满足交换律,则可以进行交换比较。
如果is_degenerate_pred_为true,则需要检查L_DS_和R_DS_是否分别与左右子表集合存在交集,则可以进行交换比较。
对CR_中的冲突检测规则进行判断,当规则左侧的表集合与左右子表集合的并集存在交集时,右侧的表集合必须是该并集的子集。
对cross_product_rule_中的冲突检测规则进行判断,当规则左侧的表集合与左右子表集合中一侧的表存在交集时,右侧的表必须是同侧子表集合的子集。
对delay_cross_product_rule_中的冲突检测规则进行判断,当规则左侧的表集合与左右子表集合中一侧的表存在交集时,右侧的表必须是同侧子表集合的子集。