继续分析 query_planner:
/** query_planner* Generate a path (that is, a simplified plan) for a basic query,* which may involve joins but not any fancier features.** Since query_planner does not handle the toplevel processing (grouping,* sorting, etc) it cannot select the best path by itself. It selects* two paths: the cheapest path that produces all the required tuples,* independent of any ordering considerations, and the cheapest path that* produces the expected fraction of the required tuples in the required* ordering, if there is a path that is cheaper for this than just sorting* the output of the cheapest overall path. The caller (grouping_planner)* will make the final decision about which to use.** Input parameters:* root describes the query to plan* tlist is the target list the query should produce* (this is NOT necessarily root->parse->targetList!)* tuple_fraction is the fraction of tuples we expect will be retrieved* limit_tuples is a hard limit on number of tuples to retrieve,* or -1 if no limit** Output parameters:* *cheapest_path receives the overall-cheapest path for the query* *sorted_path receives the cheapest presorted path for the query,* if any (NULL if there is no useful presorted path)* *num_groups receives the estimated number of groups, or 1 if query* does not use grouping** Note: the PlannerInfo node also includes a query_pathkeys field, which is* both an input and an output of query_planner(). The input value signals* query_planner that the indicated sort order is wanted in the final output* plan. But this value has not yet been "canonicalized", since the needed* info does not get computed until we scan the qual clauses. We canonicalize* it as soon as that task is done. (The main reason query_pathkeys is a* PlannerInfo field and not a passed parameter is that the low-level routines* in indxpath.c need to see it.)** Note: the PlannerInfo node includes other pathkeys fields besides* query_pathkeys, all of which need to be canonicalized once the info is* available. See canonicalize_all_pathkeys.** tuple_fraction is interpreted as follows:* 0: expect all tuples to be retrieved (normal case)* 0 < tuple_fraction < 1: expect the given fraction of tuples available* from the plan to be retrieved* tuple_fraction >= 1: tuple_fraction is the absolute number of tuples* expected to be retrieved (ie, a LIMIT specification)* Note that a nonzero tuple_fraction could come from outer context; it is* therefore not redundant with limit_tuples. We use limit_tuples to determine* whether a bounded sort can be used at runtime.*/ void query_planner(PlannerInfo *root, List *tlist,double tuple_fraction, double limit_tuples,Path **cheapest_path, Path **sorted_path,double *num_groups) {Query *parse = root->parse;List *joinlist;RelOptInfo *final_rel;Path *cheapestpath;Path *sortedpath;Index rti;double total_pages;/* Make tuple_fraction, limit_tuples accessible to lower-level routines */root->tuple_fraction = tuple_fraction;root->limit_tuples = limit_tuples;*num_groups = 1; /* default result *//** If the query has an empty join tree, then it's something easy like* "SELECT 2+2;" or "INSERT ... VALUES()". Fall through quickly.*/if (parse->jointree->fromlist == NIL){/* We need a trivial path result */*cheapest_path = (Path *)create_result_path((List *) parse->jointree->quals);*sorted_path = NULL;/** We still are required to canonicalize any pathkeys, in case it's* something like "SELECT 2+2 ORDER BY 1".*/root->canon_pathkeys = NIL;canonicalize_all_pathkeys(root);return;}/** Init planner lists to empty.** NOTE: append_rel_list was set up by subquery_planner, so do not touch* here; eq_classes and minmax_aggs may contain data already, too.*/root->join_rel_list = NIL;root->join_rel_hash = NULL;root->join_rel_level = NULL;root->join_cur_level = 0;root->canon_pathkeys = NIL;root->left_join_clauses = NIL;root->right_join_clauses = NIL;root->full_join_clauses = NIL;root->join_info_list = NIL;root->placeholder_list = NIL;root->initial_rels = NIL;/** Make a flattened version of the rangetable for faster access (this is* OK because the rangetable won't change any more), and set up an empty* array for indexing base relations.*/setup_simple_rel_arrays(root);/** Construct RelOptInfo nodes for all base relations in query, and* indirectly for all appendrel member relations ("other rels"). This* will give us a RelOptInfo for every "simple" (non-join) rel involved in* the query.** Note: the reason we find the rels by searching the jointree and* appendrel list, rather than just scanning the rangetable, is that the* rangetable may contain RTEs for rels not actively part of the query,* for example views. We don't want to make RelOptInfos for them.*/add_base_rels_to_query(root, (Node *) parse->jointree);/** Examine the targetlist and join tree, adding entries to baserel* targetlists for all referenced Vars, and generating PlaceHolderInfo* entries for all referenced PlaceHolderVars. Restrict and join clauses* are added to appropriate lists belonging to the mentioned relations. We* also build EquivalenceClasses for provably equivalent expressions. The* SpecialJoinInfo list is also built to hold information about join order* restrictions. Finally, we form a target joinlist for make_one_rel() to* work from.*/build_base_rel_tlists(root, tlist);find_placeholders_in_jointree(root);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);/** We have completed merging equivalence sets, so it's now possible to* convert previously generated pathkeys (in particular, the requested* query_pathkeys) to canonical form.*/canonicalize_all_pathkeys(root);/** Examine any "placeholder" expressions generated during subquery pullup.* Make sure that the Vars they need are marked as needed at the relevant* join level. This must be done before join removal because it might* cause Vars or placeholders to be needed above a join when they weren't* so marked before.*/fix_placeholder_input_needed_levels(root);/** Remove any useless outer joins. Ideally this would be done during* jointree preprocessing, but the necessary information isn't available* until we've built baserel data structures and classified qual clauses.*/joinlist = remove_useless_joins(root, joinlist);/** Now distribute "placeholders" to base rels as needed. This has to be* done after join removal because removal could change whether a* placeholder is evaluatable at a base rel.*/add_placeholders_to_base_rels(root);/** We should now have size estimates for every actual table involved in* the query, and we also know which if any have been deleted from the* query by join removal; so we can compute total_table_pages.** Note that appendrels are not double-counted here, even though we don't* bother to distinguish RelOptInfos for appendrel parents, because the* parents will still have size zero.** XXX if a table is self-joined, we will count it once per appearance,* which perhaps is the wrong thing ... but that's not completely clear,* and detecting self-joins here is difficult, so ignore it for now.*/total_pages = 0;for (rti = 1; rti < root->simple_rel_array_size; rti++){RelOptInfo *brel = root->simple_rel_array[rti];if (brel == NULL)continue;Assert(brel->relid == rti); /* sanity check on array */if (brel->reloptkind == RELOPT_BASEREL ||brel->reloptkind == RELOPT_OTHER_MEMBER_REL)total_pages += (double) brel->pages;}root->total_table_pages = total_pages;/** Ready to do the primary planning.*/final_rel = make_one_rel(root, joinlist);if (!final_rel || !final_rel->cheapest_total_path)elog(ERROR, "failed to construct the join relation");/** If there's grouping going on, estimate the number of result groups. We* couldn't do this any earlier because it depends on relation size* estimates that were set up above.** Then convert tuple_fraction to fractional form if it is absolute, and* adjust it based on the knowledge that grouping_planner will be doing* grouping or aggregation work with our result.** This introduces some undesirable coupling between this code and* grouping_planner, but the alternatives seem even uglier; we couldn't* pass back completed paths without making these decisions here.*/if (parse->groupClause){List *groupExprs;groupExprs = get_sortgrouplist_exprs(parse->groupClause,parse->targetList);*num_groups = estimate_num_groups(root,groupExprs,final_rel->rows);/** In GROUP BY mode, an absolute LIMIT is relative to the number of* groups not the number of tuples. If the caller gave us a fraction,* keep it as-is. (In both cases, we are effectively assuming that* all the groups are about the same size.)*/if (tuple_fraction >= 1.0)tuple_fraction /= *num_groups;/** If both GROUP BY and ORDER BY are specified, we will need two* levels of sort --- and, therefore, certainly need to read all the* tuples --- unless ORDER BY is a subset of GROUP BY. Likewise if we* have both DISTINCT and GROUP BY, or if we have a window* specification not compatible with the GROUP BY.*/if (!pathkeys_contained_in(root->sort_pathkeys, root->group_pathkeys) ||!pathkeys_contained_in(root->distinct_pathkeys, root->group_pathkeys) ||!pathkeys_contained_in(root->window_pathkeys, root->group_pathkeys))tuple_fraction = 0.0;/* In any case, limit_tuples shouldn't be specified here */Assert(limit_tuples < 0);}else if (parse->hasAggs || root->hasHavingQual){/** Ungrouped aggregate will certainly want to read all the tuples, and* it will deliver a single result row (so leave *num_groups 1).*/tuple_fraction = 0.0;/* limit_tuples shouldn't be specified here */Assert(limit_tuples < 0);}else if (parse->distinctClause){/** Since there was no grouping or aggregation, it's reasonable to* assume the UNIQUE filter has effects comparable to GROUP BY. Return* the estimated number of output rows for use by caller. (If DISTINCT* is used with grouping, we ignore its effects for rowcount* estimation purposes; this amounts to assuming the grouped rows are* distinct already.)*/List *distinctExprs;distinctExprs = get_sortgrouplist_exprs(parse->distinctClause,parse->targetList);*num_groups = estimate_num_groups(root,distinctExprs,final_rel->rows);/** Adjust tuple_fraction the same way as for GROUP BY, too.*/if (tuple_fraction >= 1.0)tuple_fraction /= *num_groups;/* limit_tuples shouldn't be specified here */Assert(limit_tuples < 0);}else{/** Plain non-grouped, non-aggregated query: an absolute tuple fraction* can be divided by the number of tuples.*/if (tuple_fraction >= 1.0)tuple_fraction /= final_rel->rows;}/** Pick out the cheapest-total path and the cheapest presorted path for* the requested pathkeys (if there is one). We should take the tuple* fraction into account when selecting the cheapest presorted path, but* not when selecting the cheapest-total path, since if we have to sort* then we'll have to fetch all the tuples. (But there's a special case:* if query_pathkeys is NIL, meaning order doesn't matter, then the* "cheapest presorted" path will be the cheapest overall for the tuple* fraction.)** The cheapest-total path is also the one to use if grouping_planner* decides to use hashed aggregation, so we return it separately even if* this routine thinks the presorted path is the winner.*/cheapestpath = final_rel->cheapest_total_path;sortedpath =get_cheapest_fractional_path_for_pathkeys(final_rel->pathlist,root->query_pathkeys,NULL,tuple_fraction);/* Don't return same path in both guises; just wastes effort */if (sortedpath == cheapestpath)sortedpath = NULL;/** Forget about the presorted path if it would be cheaper to sort the* cheapest-total path. Here we need consider only the behavior at the* tuple fraction point.*/if (sortedpath){Path sort_path; /* dummy for result of cost_sort */if (root->query_pathkeys == NIL ||pathkeys_contained_in(root->query_pathkeys,cheapestpath->pathkeys)){/* No sort needed for cheapest path */sort_path.startup_cost = cheapestpath->startup_cost;sort_path.total_cost = cheapestpath->total_cost;}else{/* Figure cost for sorting */cost_sort(&sort_path, root, root->query_pathkeys,cheapestpath->total_cost,final_rel->rows, final_rel->width,0.0, work_mem, limit_tuples);}if (compare_fractional_path_costs(sortedpath, &sort_path,tuple_fraction) > 0){/* Presorted path is a loser */sortedpath = NULL;}}*cheapest_path = cheapestpath;*sorted_path = sortedpath; }
这个就是对基本的查询的基本分析了。
下面,慢慢进行:
我的简单查询里面,有 from 子句,所以不满足如下这段,不会直接退出。
/** If the query has an empty join tree, then it's something easy like* "SELECT 2+2;" or "INSERT ... VALUES()". Fall through quickly.*/if (parse->jointree->fromlist == NIL){/* We need a trivial path result */*cheapest_path = (Path *)create_result_path((List *) parse->jointree->quals);*sorted_path = NULL;/** We still are required to canonicalize any pathkeys, in case it's* something like "SELECT 2+2 ORDER BY 1".*/root->canon_pathkeys = NIL;canonicalize_all_pathkeys(root);return;}
下面是初始化动作:
/** Init planner lists to empty.** NOTE: append_rel_list was set up by subquery_planner, so do not touch* here; eq_classes and minmax_aggs may contain data already, too.*/root->join_rel_list = NIL;root->join_rel_hash = NULL;root->join_rel_level = NULL;root->join_cur_level = 0;root->canon_pathkeys = NIL;root->left_join_clauses = NIL;root->right_join_clauses = NIL;root->full_join_clauses = NIL;root->join_info_list = NIL;root->placeholder_list = NIL;root->initial_rels = NIL;
下面这一段,也先把它当作一个简单的初始化理解:
/** Make a flattened version of the rangetable for faster access (this is* OK because the rangetable won't change any more), and set up an empty* array for indexing base relations.*/setup_simple_rel_arrays(root);
接着,这是把 from 和 where 条件的 FromExpr型指针,变换为Node型指针,挂到 root上。
/** Construct RelOptInfo nodes for all base relations in query, and* indirectly for all appendrel member relations ("other rels"). This* will give us a RelOptInfo for every "simple" (non-join) rel involved in* the query.** Note: the reason we find the rels by searching the jointree and* appendrel list, rather than just scanning the rangetable, is that the* rangetable may contain RTEs for rels not actively part of the query,* for example views. We don't want to make RelOptInfos for them.*/add_base_rels_to_query(root, (Node *) parse->jointree);
将其展开:
/******************************************************************************* JOIN TREES******************************************************************************//** add_base_rels_to_query** Scan the query's jointree and create baserel RelOptInfos for all* the base relations (ie, table, subquery, and function RTEs)* appearing in the jointree.** The initial invocation must pass root->parse->jointree as the value of* jtnode. Internally, the function recurses through the jointree.** At the end of this process, there should be one baserel RelOptInfo for* every non-join RTE that is used in the query. Therefore, this routine* is the only place that should call build_simple_rel with reloptkind* RELOPT_BASEREL. (Note: build_simple_rel recurses internally to build* "other rel" RelOptInfos for the members of any appendrels we find here.)*/ void add_base_rels_to_query(PlannerInfo *root, Node *jtnode) {if (jtnode == NULL)return;if (IsA(jtnode, RangeTblRef)){int varno = ((RangeTblRef *) jtnode)->rtindex;(void) build_simple_rel(root, varno, RELOPT_BASEREL);}else if (IsA(jtnode, FromExpr)){FromExpr *f = (FromExpr *) jtnode;ListCell *l;foreach(l, f->fromlist)add_base_rels_to_query(root, lfirst(l));}else if (IsA(jtnode, JoinExpr)){JoinExpr *j = (JoinExpr *) jtnode;add_base_rels_to_query(root, j->larg);add_base_rels_to_query(root, j->rarg);}elseelog(ERROR, "unrecognized node type: %d",(int) nodeTag(jtnode)); }
这里面,有递归调用。
主要是因为这个:就是把from 里面每个表明,都挂在root上。
foreach(l, f->fromlist)add_base_rels_to_query(root, lfirst(l));
上述的递归调用时,进了 if (IsA(jtnode, RangeTblRef)) 的判断。会调用 build_simple_rel函数,展开了看:
/** build_simple_rel* Construct a new RelOptInfo for a base relation or 'other' relation.*/ RelOptInfo * build_simple_rel(PlannerInfo *root, int relid, RelOptKind reloptkind) {RelOptInfo *rel;RangeTblEntry *rte;/* Rel should not exist already */Assert(relid > 0 && relid < root->simple_rel_array_size);if (root->simple_rel_array[relid] != NULL)elog(ERROR, "rel %d already exists", relid);/* Fetch RTE for relation */rte = root->simple_rte_array[relid];Assert(rte != NULL);rel = makeNode(RelOptInfo);rel->reloptkind = reloptkind;rel->relids = bms_make_singleton(relid);rel->rows = 0;rel->width = 0;rel->reltargetlist = NIL;rel->pathlist = NIL;rel->ppilist = NIL;rel->cheapest_startup_path = NULL;rel->cheapest_total_path = NULL;rel->cheapest_unique_path = NULL;rel->cheapest_parameterized_paths = NIL;rel->relid = relid;rel->rtekind = rte->rtekind;/* min_attr, max_attr, attr_needed, attr_widths are set below */rel->indexlist = NIL;rel->pages = 0;rel->tuples = 0;rel->allvisfrac = 0;rel->subplan = NULL;rel->subroot = NULL;rel->fdwroutine = NULL;rel->fdw_private = NULL;rel->baserestrictinfo = NIL;rel->baserestrictcost.startup = 0;rel->baserestrictcost.per_tuple = 0;rel->joininfo = NIL;rel->has_eclass_joins = false;/* Check type of rtable entry */switch (rte->rtekind){case RTE_RELATION:/* Table --- retrieve statistics from the system catalogs */get_relation_info(root, rte->relid, rte->inh, rel);break;case RTE_SUBQUERY:case RTE_FUNCTION:case RTE_VALUES:case RTE_CTE:/** Subquery, function, or values list --- set up attr range and* arrays** Note: 0 is included in range to support whole-row Vars*/rel->min_attr = 0;rel->max_attr = list_length(rte->eref->colnames);rel->attr_needed = (Relids *)palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(Relids));rel->attr_widths = (int32 *)palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(int32));break;default:elog(ERROR, "unrecognized RTE kind: %d",(int) rte->rtekind);break;}/* Save the finished struct in the query's simple_rel_array */root->simple_rel_array[relid] = rel;/** If this rel is an appendrel parent, recurse to build "other rel"* RelOptInfos for its children. They are "other rels" because they are* not in the main join tree, but we will need RelOptInfos to plan access* to them.*/if (rte->inh){ListCell *l;foreach(l, root->append_rel_list){AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(l);/* append_rel_list contains all append rels; ignore others */if (appinfo->parent_relid != relid)continue;(void) build_simple_rel(root, appinfo->child_relid,RELOPT_OTHER_MEMBER_REL);}}return rel; }
核心是这个:
/* Save the finished struct in the query's simple_rel_array */root->simple_rel_array[relid] = rel;
回到 query_planner 这个层面。
/** Examine the targetlist and join tree, adding entries to baserel* targetlists for all referenced Vars, and generating PlaceHolderInfo* entries for all referenced PlaceHolderVars. Restrict and join clauses* are added to appropriate lists belonging to the mentioned relations. We* also build EquivalenceClasses for provably equivalent expressions. The* SpecialJoinInfo list is also built to hold information about join order* restrictions. Finally, we form a target joinlist for make_one_rel() to* work from.*/build_base_rel_tlists(root, tlist);
展开:
这个和我的简单查询关系也不大,因为我的简单查询没有什么表达式要计算:
/** build_base_rel_tlists* Add targetlist entries for each var needed in the query's final tlist* to the appropriate base relations.** We mark such vars as needed by "relation 0" to ensure that they will* propagate up through all join plan steps.*/ void build_base_rel_tlists(PlannerInfo *root, List *final_tlist) {List *tlist_vars = pull_var_clause((Node *) final_tlist,PVC_RECURSE_AGGREGATES,PVC_INCLUDE_PLACEHOLDERS); if (tlist_vars != NIL){add_vars_to_targetlist(root, tlist_vars, bms_make_singleton(0), true);list_free(tlist_vars);} }
那么下面这一段:
find_placeholders_in_jointree(root);
展开后可以看到,对于我的简单查询,没有 placeholderz,所以会直接返回:
/** find_placeholders_in_jointree* Search the jointree for PlaceHolderVars, and build PlaceHolderInfos** We don't need to look at the targetlist because build_base_rel_tlists()* will already have made entries for any PHVs in the tlist.*/ void find_placeholders_in_jointree(PlannerInfo *root) {/* We need do nothing if the query contains no PlaceHolderVars */if (root->glob->lastPHId != 0){/* Start recursion at top of jointree */Assert(root->parse->jointree != NULL &&IsA(root->parse->jointree, FromExpr));(void) find_placeholders_recurse(root, (Node *) root->parse->jointree);} }
接着往下看,这个先暂时无视。
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);
再接着看下面这一段好了:
/** We have completed merging equivalence sets, so it's now possible to* convert previously generated pathkeys (in particular, the requested* query_pathkeys) to canonical form.*/canonicalize_all_pathkeys(root);
展开:
/** canonicalize_all_pathkeys* Canonicalize all pathkeys that were generated before entering* query_planner and then stashed in PlannerInfo.*/ static void canonicalize_all_pathkeys(PlannerInfo *root) {root->query_pathkeys = canonicalize_pathkeys(root, root->query_pathkeys);root->group_pathkeys = canonicalize_pathkeys(root, root->group_pathkeys);root->window_pathkeys = canonicalize_pathkeys(root, root->window_pathkeys);root->distinct_pathkeys = canonicalize_pathkeys(root, root->distinct_pathkeys);root->sort_pathkeys = canonicalize_pathkeys(root, root->sort_pathkeys); }
/** canonicalize_pathkeys* Convert a not-necessarily-canonical pathkeys list to canonical form.** Note that this function must not be used until after we have completed* merging EquivalenceClasses.*/ List * canonicalize_pathkeys(PlannerInfo *root, List *pathkeys) {List *new_pathkeys = NIL;ListCell *l;foreach(l, pathkeys){PathKey *pathkey = (PathKey *) lfirst(l);EquivalenceClass *eclass;PathKey *cpathkey;/* Find the canonical (merged) EquivalenceClass */eclass = pathkey->pk_eclass;while (eclass->ec_merged)eclass = eclass->ec_merged;/** If we can tell it's redundant just from the EC, skip.* pathkey_is_redundant would notice that, but we needn't even bother* constructing the node...*/if (EC_MUST_BE_REDUNDANT(eclass))continue;/* OK, build a canonicalized PathKey struct */cpathkey = make_canonical_pathkey(root,eclass,pathkey->pk_opfamily,pathkey->pk_strategy,pathkey->pk_nulls_first);/* Add to list unless redundant */if (!pathkey_is_redundant(cpathkey, new_pathkeys))new_pathkeys = lappend(new_pathkeys, cpathkey);}return new_pathkeys; }
到这一步,心里有个感触,是否我需要把所有的 sql文再复习一遍?
由于我的简单查询 root->query_pathkeys 是NG, 所以就直接返回了。
接着再分析下一个调用:
这下面三段,都可以无视:
/** Examine any "placeholder" expressions generated during subquery pullup.* Make sure that the Vars they need are marked as needed at the relevant* join level. This must be done before join removal because it might* cause Vars or placeholders to be needed above a join when they weren't* so marked before.*/fix_placeholder_input_needed_levels(root);/** Remove any useless outer joins. Ideally this would be done during* jointree preprocessing, but the necessary information isn't available* until we've built baserel data structures and classified qual clauses.*/joinlist = remove_useless_joins(root, joinlist);/** Now distribute "placeholders" to base rels as needed. This has to be* done after join removal because removal could change whether a* placeholder is evaluatable at a base rel.*/add_placeholders_to_base_rels(root);
再接着分析:
/** We should now have size estimates for every actual table involved in* the query, and we also know which if any have been deleted from the* query by join removal; so we can compute total_table_pages.** Note that appendrels are not double-counted here, even though we don't* bother to distinguish RelOptInfos for appendrel parents, because the* parents will still have size zero.** XXX if a table is self-joined, we will count it once per appearance,* which perhaps is the wrong thing ... but that's not completely clear,* and detecting self-joins here is difficult, so ignore it for now.*/total_pages = 0;for (rti = 1; rti < root->simple_rel_array_size; rti++){RelOptInfo *brel = root->simple_rel_array[rti];if (brel == NULL)continue;Assert(brel->relid == rti); /* sanity check on array */if (brel->reloptkind == RELOPT_BASEREL ||brel->reloptkind == RELOPT_OTHER_MEMBER_REL)total_pages += (double) brel->pages;}root->total_table_pages = total_pages;
这是说,要得到总共的 页面数,好作为计划的依据。
但是我执行到此处,令我惊诧的是,得到的 root->total_table_pages 居然是0!
接下来的应该算是戏肉吧:
/** Ready to do the primary planning.*/final_rel = make_one_rel(root, joinlist);