diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index c134594a21..4b1c547767 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -135,6 +135,11 @@ static Path *get_singleton_append_subpath(Path *path, static void set_dummy_rel_pathlist(RelOptInfo *rel); static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); +static void add_subqueryscan_variant(PlannerInfo *root, RelOptInfo *rel, + Index rti, RangeTblEntry *rte, + Bitmapset *required_outer, + Query *subquery, List *pushed_down_clauses, double tuple_fraction, + bool update_estimates); static void set_function_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte); static void set_values_pathlist(PlannerInfo *root, RelOptInfo *rel, @@ -163,13 +168,12 @@ static pushdown_safe_type qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo, pushdown_safety_info *safetyInfo); static void subquery_push_qual(Query *subquery, - RangeTblEntry *rte, Index rti, Node *qual); + RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up); static void recurse_push_qual(Node *setOp, Query *topquery, - RangeTblEntry *rte, Index rti, Node *qual); + RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up); static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel, Bitmapset *extra_used_attrs); - /* * make_one_rel * Finds all possible access paths for executing a query, returning a @@ -2674,12 +2678,18 @@ check_and_push_window_quals(Query *subquery, Node *clause, * So the paths made here will be parameterized if the subquery contains * LATERAL references, otherwise not. As long as that's true, there's no need * for a separate set_subquery_size phase: just make the paths right away. + * + * (If a subquery is LATERAL, though, we do push down join clauses that refer + * to relations that the subquery already references laterally. Pushing down + * such quals won't make the subquery any more lateral, so there's no reason + * not to.) */ static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte) { Query *parse = root->parse; + Query *unparameterized_subquery; Query *subquery = rte->subquery; bool trivial_pathtarget; Relids required_outer; @@ -2689,6 +2699,8 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Bitmapset *run_cond_attrs = NULL; ListCell *lc; char *plan_name; + List *pushed_down_ec_joins = NIL; + bool sq_is_pushdown_safe; /* * Must copy the Query so that planning doesn't mess up the RTE contents @@ -2699,8 +2711,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, /* * If it's a LATERAL subquery, it might contain some Vars of the current - * query level, requiring it to be treated as parameterized, even though - * we don't support pushing down join quals into subqueries. + * query level, requiring it to be treated as parameterized. */ required_outer = rel->lateral_relids; @@ -2739,65 +2750,143 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, * pseudoconstant clauses; better to have the gating node above the * subquery. * + * Join clauses are only pushed down, if the subquery is LATERAL, and + * the join clause only refers to relations that the subquery already + * depends on. It might be useful to push down other join clauses, too, + * but then we would need to plan the subquery multiple times, to create + * parameterized paths, which seems too expensive. + * * Non-pushed-down clauses will get evaluated as qpquals of the * SubqueryScan node. * * XXX Are there any cases where we want to make a policy decision not to * push down a pushable qual, because it'd result in a worse plan? */ - if (rel->baserestrictinfo != NIL && - subquery_is_pushdown_safe(subquery, subquery, &safetyInfo)) + sq_is_pushdown_safe = subquery_is_pushdown_safe(subquery, subquery, &safetyInfo); + if (sq_is_pushdown_safe && + (rel->baserestrictinfo != NIL || + (!bms_is_empty(required_outer) && (rel->joininfo || rel->has_eclass_joins)))) { /* OK to consider pushing down individual quals */ - List *upperrestrictlist = NIL; ListCell *l; + Bitmapset *available_relids; - foreach(l, rel->baserestrictinfo) + if (rel->baserestrictinfo) { - RestrictInfo *rinfo = (RestrictInfo *) lfirst(l); - Node *clause = (Node *) rinfo->clause; + List *upperrestrictlist = NIL; - if (rinfo->pseudoconstant) - { - upperrestrictlist = lappend(upperrestrictlist, rinfo); - continue; - } - switch (qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo)) + foreach(l, rel->baserestrictinfo) { - case PUSHDOWN_SAFE: - /* Push it down */ - subquery_push_qual(subquery, rte, rti, clause); - break; + RestrictInfo *rinfo = (RestrictInfo *) lfirst(l); + Node *clause = (Node *) rinfo->clause; - case PUSHDOWN_WINDOWCLAUSE_RUNCOND: + if (rinfo->pseudoconstant) + { + upperrestrictlist = lappend(upperrestrictlist, rinfo); + continue; + } + + switch (qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo)) + { + case PUSHDOWN_SAFE: + /* Push it down */ + subquery_push_qual(subquery, rte, rti, clause, 0); + break; + + case PUSHDOWN_WINDOWCLAUSE_RUNCOND: - /* - * Since we can't push the qual down into the subquery, - * check if it happens to reference a window function. If - * so then it might be useful to use for the WindowAgg's - * runCondition. - */ - if (!subquery->hasWindowFuncs || - check_and_push_window_quals(subquery, clause, - &run_cond_attrs)) - { /* - * subquery has no window funcs or the clause is not a - * suitable window run condition qual or it is, but - * the original must also be kept in the upper query. - */ + * Since we can't push the qual down into the subquery, + * check if it happens to reference a window function. If + * so then it might be useful to use for the WindowAgg's + * runCondition. + */ + if (!subquery->hasWindowFuncs || + check_and_push_window_quals(subquery, clause, + &run_cond_attrs)) + { + /* + * subquery has no window funcs or the clause is not a + * suitable window run condition qual or it is, but + * the original must also be kept in the upper query. + */ + upperrestrictlist = lappend(upperrestrictlist, rinfo); + } + break; + + case PUSHDOWN_UNSAFE: + /* Keep it in the upper query */ upperrestrictlist = lappend(upperrestrictlist, rinfo); + break; + } + } + } + + /* + * Push down join quals, as well. But only for LATERAL, and only for those + * relations that are "required" anyway. + */ + /* XXX: check enable_join_predicate_pushdown ? */ + if (!bms_is_empty(required_outer)) + { + available_relids = bms_copy(required_outer); + available_relids = bms_add_member(available_relids, rti); + + if (rel->joininfo) + { + ListCell *lc; + List *upperjoinlist = NIL; + + foreach(lc, rel->joininfo) + { + RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); + Node *clause = (Node *) rinfo->clause; + + if (!rinfo->pseudoconstant && + bms_is_subset(rinfo->required_relids, available_relids) && + qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo)) + { + /* Push it down */ + subquery_push_qual(subquery, rte, rti, clause, 0); + } + else + { + /* Keep it in the upper query */ + upperjoinlist = lappend(upperjoinlist, rinfo); } - break; + } + rel->joininfo = upperjoinlist; + } - case PUSHDOWN_UNSAFE: - upperrestrictlist = lappend(upperrestrictlist, rinfo); - break; + if (rel->has_eclass_joins) + { + List *clauses; + ListCell *lc; + + clauses = generate_join_implied_equalities(root, + available_relids, + required_outer, + rel, + NULL); + + foreach(lc, clauses) + { + RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); + Node *clause = (Node *) rinfo->clause; + + if (!rinfo->pseudoconstant && + qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo)) + { + /* Push it down */ + Assert(bms_is_subset(rinfo->required_relids, available_relids)); + subquery_push_qual(subquery, rte, rti, clause, 0); + + pushed_down_ec_joins = lappend(pushed_down_ec_joins, clause); + } + } } } - rel->baserestrictinfo = upperrestrictlist; - /* We don't bother recomputing baserestrict_min_security */ } pfree(safetyInfo.unsafeFlags); @@ -2827,16 +2916,115 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, else tuple_fraction = root->tuple_fraction; + unparameterized_subquery = copyObject(subquery); + + add_subqueryscan_variant(root, rel, rti, rte, + required_outer, subquery, pushed_down_ec_joins, tuple_fraction, true); + + /* + * Also create parameterized join paths, where we push the join condition + * down to the subquery. + * + * To keep the planning time reasonable, this is all-or-nothing. We try to + * push all join conditions down to the subquery, and create paths for that. + * We don't create paths for every combination of join conditions that we + * could push down. + */ + if ((rel->has_eclass_joins || rel->joininfo) && + sq_is_pushdown_safe) + { + List *clauses; + ListCell *lc; + List *pushed_down_clauses = list_copy(pushed_down_ec_joins); + Bitmapset *available_relids; + Bitmapset *other_relids; + + subquery = copyObject(unparameterized_subquery); + + required_outer = bms_copy(required_outer); + + foreach(lc, rel->joininfo) + { + RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); + Node *clause = (Node *) rinfo->clause; + + if (!rinfo->pseudoconstant && + qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo)) + { + /* Push it down */ + required_outer = bms_union(required_outer, + pull_varnos(root, clause)); + required_outer = bms_del_member(required_outer, rti); + + subquery_push_qual(subquery, rte, rti, clause, 0); + + pushed_down_clauses = lappend(pushed_down_clauses, rinfo); + } + } + + /* + * We already pushed down any join quals with LATERAL referenced rels, don't add + * them again. + */ + available_relids = bms_difference(root->all_baserels, rel->lateral_referencers); + other_relids = bms_del_member(bms_copy(available_relids), rti); + + clauses = generate_join_implied_equalities(root, + available_relids, + other_relids, + rel, + NULL); + foreach(lc, clauses) + { + RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); + Node *clause = (Node *) rinfo->clause; + + if (!rinfo->pseudoconstant && + qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo)) + { + /* Push it down */ + required_outer = bms_union(required_outer, + pull_varnos(root, clause)); + required_outer = bms_del_member(required_outer, rti); + + subquery_push_qual(subquery, rte, rti, clause, 0); + + pushed_down_clauses = lappend(pushed_down_clauses, rinfo); + } + } + if (pushed_down_clauses) + add_subqueryscan_variant(root, rel, rti, rte, + required_outer, + subquery, pushed_down_clauses, tuple_fraction, false); + } + + pfree(safetyInfo.unsafeFlags); +} + +static void +add_subqueryscan_variant(PlannerInfo *root, RelOptInfo *rel, + Index rti, RangeTblEntry *rte, + Bitmapset *required_outer, + Query *subquery, List *pushed_down_clauses, double tuple_fraction, + bool update_estimates) +{ + RelOptInfo *sub_final_rel; + ListCell *lc; + PlannerInfo *subroot; + List *subplan_params; + char *plan_name; + bool trivial_pathtarget; + /* plan_params should not be in use in current query level */ Assert(root->plan_params == NIL); /* Generate a subroot and Paths for the subquery */ plan_name = choose_plan_name(root->glob, rte->eref->aliasname, false); - rel->subroot = subquery_planner(root->glob, subquery, plan_name, + subroot = subquery_planner(root->glob, subquery, plan_name, root, NULL, false, tuple_fraction, NULL); /* Isolate the params needed by this specific subplan */ - rel->subplan_params = root->plan_params; + subplan_params = root->plan_params; root->plan_params = NIL; /* @@ -2844,7 +3032,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, * so, it's desirable to produce an unadorned dummy path so that we will * recognize appropriate optimizations at this query level. */ - sub_final_rel = fetch_upper_rel(rel->subroot, UPPERREL_FINAL, NULL); + sub_final_rel = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL); if (IS_DUMMY_REL(sub_final_rel)) { @@ -2856,8 +3044,13 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, * Mark rel with estimated output rows, width, etc. Note that we have to * do this before generating outer-query paths, else cost_subqueryscan is * not happy. + * + * Don't overwrite the estimates when we're creating parameterized paths + * for joins. The estimate for a parameterized path includes the effects + * of the join clauses. */ - set_subquery_size_estimates(root, rel); + if (update_estimates) + set_subquery_size_estimates(root, rel, subroot); /* * Also detect whether the reltarget is trivial, so that we can pass that @@ -2908,7 +3101,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, add_path(rel, (Path *) create_subqueryscan_path(root, rel, subpath, trivial_pathtarget, - pathkeys, required_outer)); + pathkeys, required_outer, pushed_down_clauses)); } /* If outer rel allows parallelism, do same for partial paths. */ @@ -2932,10 +3125,10 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, /* Generate outer path using this subpath */ add_partial_path(rel, (Path *) - create_subqueryscan_path(root, rel, subpath, + create_subqueryscan_path(root, rel, subroot, subplan_params, subpath, trivial_pathtarget, pathkeys, - required_outer)); + required_outer, pushed_down_clauses)); } } } @@ -4505,6 +4698,13 @@ qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo, break; } + /* + * In a restriction clause, all Vars must refer to subselect output + * columns, but join quals will contain Vars referring to other relations. + */ + if (var->varno != rti) + continue; + /* Subqueries have no system columns */ Assert(var->varattno >= 0); @@ -4543,13 +4743,13 @@ qual_is_pushdown_safe(Query *subquery, Index rti, RestrictInfo *rinfo, * subquery_push_qual - push down a qual that we have determined is safe */ static void -subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual) +subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up) { if (subquery->setOperations != NULL) { /* Recurse to push it separately to each component query */ recurse_push_qual(subquery->setOperations, subquery, - rte, rti, qual); + rte, rti, qual, sublevels_up); } else { @@ -4557,12 +4757,18 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual) * We need to replace Vars in the qual (which must refer to outputs of * the subquery) with copies of the subquery's targetlist expressions. * Note that at this point, any uplevel Vars in the qual should have - * been replaced with Params, so they need no work. + * been replaced with Params, so they need no work. But in a join qual, + * there can be Vars referring to other relations at the same level. + * We need to increment varlevelsup of those, so that when the qual is + * pushed down, they refer to the parent query. * * This step also ensures that when we are pushing into a setop tree, * each component query gets its own copy of the qual. */ - qual = ReplaceVarsFromTargetList(qual, rti, 0, rte, + qual = copyObject(qual); + IncrementVarSublevelsUp(qual, sublevels_up + 1, 0); + + qual = ReplaceVarsFromTargetList(qual, rti, sublevels_up + 1, rte, subquery->targetList, subquery->resultRelation, REPLACEVARS_REPORT_ERROR, 0, @@ -4592,7 +4798,7 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual) */ static void recurse_push_qual(Node *setOp, Query *topquery, - RangeTblEntry *rte, Index rti, Node *qual) + RangeTblEntry *rte, Index rti, Node *qual, int sublevels_up) { if (IsA(setOp, RangeTblRef)) { @@ -4601,14 +4807,14 @@ recurse_push_qual(Node *setOp, Query *topquery, Query *subquery = subrte->subquery; Assert(subquery != NULL); - subquery_push_qual(subquery, rte, rti, qual); + subquery_push_qual(subquery, rte, rti, qual, sublevels_up + 1); } else if (IsA(setOp, SetOperationStmt)) { SetOperationStmt *op = (SetOperationStmt *) setOp; - recurse_push_qual(op->larg, topquery, rte, rti, qual); - recurse_push_qual(op->rarg, topquery, rte, rti, qual); + recurse_push_qual(op->larg, topquery, rte, rti, qual, sublevels_up); + recurse_push_qual(op->rarg, topquery, rte, rti, qual, sublevels_up); } else { diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 1c575e56ff..879d00968b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -146,6 +146,7 @@ int max_parallel_workers_per_gather = 2; bool enable_seqscan = true; bool enable_indexscan = true; bool enable_indexonlyscan = true; +bool enable_join_predicate_pushdown = true; bool enable_bitmapscan = true; bool enable_tidscan = true; bool enable_sort = true; @@ -6044,9 +6045,8 @@ get_foreign_key_join_selectivity(PlannerInfo *root, * We set the same fields as set_baserel_size_estimates. */ void -set_subquery_size_estimates(PlannerInfo *root, RelOptInfo *rel) +set_subquery_size_estimates(PlannerInfo *root, RelOptInfo *rel, PlannerInfo *subroot) { - PlannerInfo *subroot = rel->subroot; RelOptInfo *sub_final_rel; ListCell *lc; diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index de6a183da7..f5545dad80 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3544,17 +3544,69 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path, RelOptInfo *rel = best_path->path.parent; Index scan_relid = rel->relid; Plan *subplan; + ListCell *l; + List *qpqual; + List *sq_quals = best_path->pushed_down_clauses; /* it should be a subquery base rel... */ Assert(scan_relid > 0); Assert(rel->rtekind == RTE_SUBQUERY); + Assert(rel->chosen_plan == NULL); /* * Recursively create Plan from Path for subquery. Since we are entering * a different planner context (subroot), recurse to create_plan not * create_plan_recurse. */ - subplan = create_plan(rel->subroot, best_path->subpath); + subplan = create_plan(best_path->subroot, best_path->subpath); + + /* + * If this path used join quals that were pushed down to the subquery, + * we don't need to re-check those quals on the SubqueryScan node itself. + */ + if (best_path->pushed_down_clauses) + { + List *new_clauses = NIL; + ListCell *l; + + foreach(l, scan_clauses) + { + RestrictInfo *rinfo = lfirst_node(RestrictInfo, l); + + if (list_member_ptr(best_path->pushed_down_clauses, rinfo)) + continue; + + new_clauses = lappend(new_clauses, rinfo); + } + scan_clauses = new_clauses; + } + + /* + * If we had pushed down any join clauses to the subquery, we don't need + * to re-check them in the SubqueryScan node. + * + * This only applies to join clauses derived from equivalence classes. + * Non-join quals, and non-EC-derived join clauses are immediately removed + * from 'baserestrictinfo' and 'joininfo' when they're pushed down, so we + * won't need to worry about them here. + */ + qpqual = NIL; + foreach (l, scan_clauses) + { + RestrictInfo *rinfo = lfirst_node(RestrictInfo, l); + + if (rinfo->pseudoconstant) + continue; /* we may drop pseudoconstants here */ + if (list_member_ptr(sq_quals, rinfo)) + continue; /* simple duplicate */ + if (is_redundant_derived_clause(rinfo, sq_quals)) + continue; /* derived from same EquivalenceClass */ + if (!contain_mutable_functions((Node *) rinfo->clause) && + predicate_implied_by(list_make1(rinfo->clause), sq_quals, false)) + continue; /* provably implied by indexquals */ + qpqual = lappend(qpqual, rinfo); + } + scan_clauses = qpqual; /* Sort clauses into best execution order */ scan_clauses = order_qual_clauses(root, scan_clauses); @@ -3575,7 +3627,7 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path, if (best_path->path.param_info) { process_subquery_nestloop_params(root, - rel->subplan_params); + best_path->subplan_params); scan_clauses = (List *) replace_nestloop_params(root, (Node *) scan_clauses); } @@ -3587,6 +3639,8 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path, copy_generic_path_info(&scan_plan->scan.plan, &best_path->path); + rel->chosen_plan = best_path->subroot; + return scan_plan; } diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index ff0e875f2a..dadda43639 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -488,12 +488,12 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing) * that some upper query level is treating this one as dummy, * and so we won't scan this level's plan tree at all. */ - if (rel->subroot == NULL) + if (rel->chosen_plan == NULL) flatten_unplanned_rtes(glob, rte); else if (recursing || - IS_DUMMY_REL(fetch_upper_rel(rel->subroot, + IS_DUMMY_REL(fetch_upper_rel(rel->chosen_plan, UPPERREL_FINAL, NULL))) - add_rtes_to_flat_rtable(rel->subroot, true); + add_rtes_to_flat_rtable(rel->chosen_plan, true); } } rti++; @@ -1460,7 +1460,7 @@ set_subqueryscan_references(PlannerInfo *root, rel = find_base_rel(root, plan->scan.scanrelid); /* Recursively process the subplan */ - plan->subplan = set_plan_references(rel->subroot, plan->subplan); + plan->subplan = set_plan_references(rel->chosen_plan, plan->subplan); if (trivial_subqueryscan(plan)) { diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 6aa8971c95..80570643b3 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -2735,11 +2735,11 @@ finalize_plan(PlannerInfo *root, Plan *plan, /* We must run finalize_plan on the subquery */ rel = find_base_rel(root, sscan->scan.scanrelid); - subquery_params = rel->subroot->outer_params; + subquery_params = rel->chosen_plan->outer_params; if (gather_param >= 0) subquery_params = bms_add_member(bms_copy(subquery_params), gather_param); - finalize_plan(rel->subroot, sscan->subplan, gather_param, + finalize_plan(rel->chosen_plan, sscan->subplan, gather_param, subquery_params, NULL); /* Now we can add its extParams to the parent's params */ diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index d1f022c5bf..b8b1ecd4e9 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -249,7 +249,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root, * suitably-sorted Paths. */ plan_name = choose_plan_name(root->glob, "setop", true); - subroot = rel->subroot = subquery_planner(root->glob, subquery, + subroot = subquery_planner(root->glob, subquery, plan_name, root, NULL, false, root->tuple_fraction, parentOp); @@ -491,7 +491,7 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, List *interesting_pathkeys, double *pNumGroups) { RelOptInfo *final_rel; - List *setop_pathkeys = rel->subroot->setop_pathkeys; + List *setop_pathkeys = subroot->setop_pathkeys; ListCell *lc; /* it can't be a set op child rel if it's not a subquery */ @@ -509,13 +509,13 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, * do this before generating outer-query paths, else cost_subqueryscan is * not happy. */ - set_subquery_size_estimates(root, rel); + set_subquery_size_estimates(root, rel, subroot); /* * Since we may want to add a partial path to this relation, we must set * its consider_parallel flag correctly. */ - final_rel = fetch_upper_rel(rel->subroot, UPPERREL_FINAL, NULL); + final_rel = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL); rel->consider_parallel = final_rel->consider_parallel; /* Generate subquery scan paths for any interesting path in final_rel */ @@ -548,10 +548,12 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, /* Generate outer path using this subpath */ add_path(rel, (Path *) create_subqueryscan_path(root, rel, + rel->subroot, + NIL, subpath, trivial_tlist, pathkeys, - NULL)); + NULL, NIL)); } /* skip dealing with sorted paths if the setop doesn't need them */ @@ -616,10 +618,12 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, /* Generate outer path using this subpath */ add_path(rel, (Path *) create_subqueryscan_path(root, rel, + rel->subroot, + NIL, subpath, trivial_tlist, pathkeys, - NULL)); + NULL, NIL)); } } @@ -640,9 +644,9 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, partial_subpath = linitial(final_rel->partial_pathlist); partial_path = (Path *) - create_subqueryscan_path(root, rel, partial_subpath, + create_subqueryscan_path(root, rel, rel->subroot, NIL, partial_subpath, trivial_tlist, - NIL, NULL); + NIL, NULL, NIL); add_partial_path(rel, partial_path); } diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 73518c8f87..913082795d 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -1906,9 +1906,12 @@ create_gather_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, * efficiently (or at least amortize it over multiple calls). */ SubqueryScanPath * -create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, +create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, + PlannerInfo *subroot, + List *subplan_params, + Path *subpath, bool trivial_pathtarget, - List *pathkeys, Relids required_outer) + List *pathkeys, Relids required_outer, List *pushed_down_ec_joins) { SubqueryScanPath *pathnode = makeNode(SubqueryScanPath); @@ -1923,6 +1926,9 @@ create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, pathnode->path.parallel_workers = subpath->parallel_workers; pathnode->path.pathkeys = pathkeys; pathnode->subpath = subpath; + pathnode->subplan_params = subplan_params; + pathnode->subroot = subroot; + pathnode->pushed_down_clauses = pushed_down_clauses; cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info, trivial_pathtarget); @@ -3977,9 +3983,12 @@ reparameterize_path(PlannerInfo *root, Path *path, return (Path *) create_subqueryscan_path(root, rel, subpath, + spath->subroot, + spath->subplan_params, trivial_pathtarget, spath->path.pathkeys, - required_outer); + required_outer, + spath->pushed_down_clauses); } case T_Result: /* Supported only for RTE_RESULT scan paths */ diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 687e923c46..adbed0d054 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -1072,6 +1072,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, joinrel->subroot = NULL; joinrel->subplan_params = NIL; joinrel->amflags = 0; + joinrel->chosen_plan = NULL; joinrel->serverid = InvalidOid; joinrel->userid = InvalidOid; joinrel->useridiscurrent = false; diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c index 9aa7ef6047..1603e493f2 100644 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -1758,6 +1758,7 @@ typedef struct int result_relation; ReplaceVarsNoMatchOption nomatch_option; int nomatch_varno; + int min_sublevels_up; } ReplaceVarsFromTargetList_context; static Node * @@ -1774,9 +1775,10 @@ ReplaceVarsFromTargetList_callback(const Var *var, rcon->nomatch_option, rcon->nomatch_varno); - /* Must adjust varlevelsup if replaced Var is within a subquery */ - if (var->varlevelsup > 0) - IncrementVarSublevelsUp(newnode, var->varlevelsup, 0); + /* Must adjust varlevelsup if tlist item is from higher query */ + if (var->varlevelsup + rcon->min_sublevels_up > 0) + IncrementVarSublevelsUp((Node *) newnode, var->varlevelsup - rcon->min_sublevels_up, 0); + return newnode; } @@ -1967,6 +1969,7 @@ ReplaceVarsFromTargetList(Node *node, context.result_relation = result_relation; context.nomatch_option = nomatch_option; context.nomatch_varno = nomatch_varno; + context.min_sublevels_up = sublevels_up; return replace_rte_variables(node, target_varno, sublevels_up, ReplaceVarsFromTargetList_callback, diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index cbc70fde71..2e4e3f42ca 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -6111,6 +6111,7 @@ examine_simple_variable(PlannerInfo *root, Var *var, Query *subquery; List *subtlist; TargetEntry *ste; + PlannerInfo *subroot; /* * Punt if it's a whole-row var rather than a plain column reference. @@ -6182,9 +6183,23 @@ examine_simple_variable(PlannerInfo *root, Var *var, subroot = list_nth(root->glob->subroots, plan_id - 1); } - /* If the subquery hasn't been planned yet, we have to punt */ - if (subroot == NULL) + if (rel->chosen_plan) + subroot = rel->chosen_plan; + else if (rel->pathlist && IsA(linitial(rel->pathlist), SubqueryScanPath)) + { + /* + * Use the estimates from the first path. XXX: what if it's a parameterized + * path? + */ + SubqueryScanPath *sqpath = (SubqueryScanPath *) linitial(rel->pathlist); + + subroot = sqpath->subroot; + } + else + { + /* If the subquery hasn't been planned yet, we have to punt */ return; + } Assert(IsA(subroot, PlannerInfo)); /* diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 3c1e6b31bf..155c7f4374 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -940,6 +940,13 @@ boot_val => 'true', }, +{ name => 'enable_join_predicate_pushdown', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables pusing down join in subqueries.', + flags => 'GUC_EXPLAIN', + variable => 'enable_join_predicate_pushdown', + boot_val => 'true', +}, + { name => 'enable_material', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', short_desc => 'Enables the planner\'s use of materialization.', flags => 'GUC_EXPLAIN', @@ -3653,3 +3660,4 @@ }, ] + diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 27a2c6815b..dd7a00ce9c 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1097,8 +1097,7 @@ typedef struct RelOptInfo double allvisfrac; /* indexes in PlannerInfo's eq_classes list of ECs that mention this rel */ Bitmapset *eclass_indexes; - PlannerInfo *subroot; /* if subquery */ - List *subplan_params; /* if subquery */ + PlannerInfo *chosen_plan; /* wanted number of parallel workers */ int rel_parallel_workers; /* Bitmask of optional features supported by the table AM */ @@ -2194,6 +2193,9 @@ typedef struct SubqueryScanPath { Path path; Path *subpath; /* path representing subquery execution */ + PlannerInfo *subroot; /* */ + List *subplan_params; /* */ + List *pushed_down_joins; /* pushed-down quals */ } SubqueryScanPath; /* diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index f2fd5d3150..e516bee612 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -52,6 +52,7 @@ extern PGDLLIMPORT int max_parallel_workers_per_gather; extern PGDLLIMPORT bool enable_seqscan; extern PGDLLIMPORT bool enable_indexscan; extern PGDLLIMPORT bool enable_indexonlyscan; +extern PGDLLIMPORT bool enable_join_predicate_pushdown; extern PGDLLIMPORT bool enable_bitmapscan; extern PGDLLIMPORT bool enable_tidscan; extern PGDLLIMPORT bool enable_sort; @@ -208,7 +209,7 @@ extern void set_joinrel_size_estimates(PlannerInfo *root, RelOptInfo *rel, RelOptInfo *inner_rel, SpecialJoinInfo *sjinfo, List *restrictlist); -extern void set_subquery_size_estimates(PlannerInfo *root, RelOptInfo *rel); +extern void set_subquery_size_estimates(PlannerInfo *root, RelOptInfo *rel, PlannerInfo *subroot); extern void set_function_size_estimates(PlannerInfo *root, RelOptInfo *rel); extern void set_values_size_estimates(PlannerInfo *root, RelOptInfo *rel); extern void set_cte_size_estimates(PlannerInfo *root, RelOptInfo *rel, diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index e8db321f92..d779ea4cbe 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -136,10 +136,13 @@ extern GatherMergePath *create_gather_merge_path(PlannerInfo *root, double *rows); extern SubqueryScanPath *create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, + PlannerInfo *subroot, + List *subplan_params, Path *subpath, bool trivial_pathtarget, List *pathkeys, - Relids required_outer); + Relids required_outer, + List *pushed_down_ec_joins); extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel, List *pathkeys, Relids required_outer); extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel, diff --git a/src/test/regress/expected/subselect_pushdown.out b/src/test/regress/expected/subselect_pushdown.out new file mode 100644 index 0000000000..792f66e7fd --- /dev/null +++ b/src/test/regress/expected/subselect_pushdown.out @@ -0,0 +1,90 @@ +-- Test pushdown of quals into subqueries. +create table smalltab (i int4, j int4); +create table bigtab (i int4, j int4); +insert into smalltab values (1, 1), (100000, 100000); +insert into bigtab select g,g from generate_series(1, 100000) g; +analyze smalltab, bigtab; +create index bigtab_i on bigtab (i); +-- Push down restriction quals. +explain (costs off) +select * from smalltab, +( + select bigtab.i, avg(bigtab.j) + from bigtab + group by bigtab.i +) as subq(i, avg) +where smalltab.i = subq.i and smalltab.i = 123; + QUERY PLAN +------------------------------------------------- + Nested Loop + -> Seq Scan on smalltab + Filter: (i = 123) + -> GroupAggregate + Group Key: bigtab.i + -> Index Scan using bigtab_i on bigtab + Index Cond: (i = 123) +(7 rows) + +-- Push down join quals. +explain (costs off) +select * from smalltab, +( + select bigtab.i, avg(bigtab.j) + from bigtab + group by bigtab.i +) as subq(i, avg) +where smalltab.i = subq.i; + QUERY PLAN +------------------------------------------------- + Nested Loop + -> Seq Scan on smalltab + -> GroupAggregate + Group Key: bigtab.i + -> Index Scan using bigtab_i on bigtab + Index Cond: (smalltab.i = i) +(6 rows) + +-- Subquery is LATERAL, and already references the other relation. The join +-- qual is always pushed down in that case, as the plan is "parameterized" +-- in respect to the other relation even if it was not pushed down. +explain (costs off) +select * from smalltab, +lateral ( + select bigtab.i, avg(bigtab.j) + from bigtab + where bigtab.j = smalltab.j + group by bigtab.i +) as subq(i, avg) +where smalltab.i < subq.i; + QUERY PLAN +------------------------------------------------- + Nested Loop + -> Seq Scan on smalltab + -> GroupAggregate + Group Key: bigtab.i + -> Index Scan using bigtab_i on bigtab + Index Cond: (smalltab.i < i) + Filter: (j = smalltab.j) +(7 rows) + +-- Multiple join clauses constructed from equivalence classes +explain (costs off) +select * from smalltab, +lateral ( + select bigtab.i, bigtab.j, avg(bigtab.j) + from bigtab + where bigtab.j/2 = smalltab.j / 2 + group by bigtab.i, bigtab.j +) as subq(i, j, avg) +where smalltab.i = subq.i and smalltab.j = subq.j; + QUERY PLAN +--------------------------------------------------------------------------- + Nested Loop + -> Seq Scan on smalltab + -> GroupAggregate + Group Key: bigtab.i, bigtab.j + -> Index Scan using bigtab_i on bigtab + Index Cond: (smalltab.i = i) + Filter: ((smalltab.j = j) AND ((j / 2) = (smalltab.j / 2))) +(7 rows) + diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47f..ec70cd2532 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -78,6 +78,10 @@ test: brin_bloom brin_multi # ---------- test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions nls sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps generated_virtual + +test: subselect_pushdown + + # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other # psql depends on create_am # amutils depends on geometry, create_index_spgist, hash_index, brin diff --git a/src/test/regress/sql/subselect_pushdown.sql b/src/test/regress/sql/subselect_pushdown.sql new file mode 100644 index 0000000000..d6431d314c --- /dev/null +++ b/src/test/regress/sql/subselect_pushdown.sql @@ -0,0 +1,55 @@ +-- Test pushdown of quals into subqueries. + +create table smalltab (i int4, j int4); +create table bigtab (i int4, j int4); + +insert into smalltab values (1, 1), (100000, 100000); +insert into bigtab select g,g from generate_series(1, 100000) g; + +analyze smalltab, bigtab; + +create index bigtab_i on bigtab (i); + +-- Push down restriction quals. +explain (costs off) +select * from smalltab, +( + select bigtab.i, avg(bigtab.j) + from bigtab + group by bigtab.i +) as subq(i, avg) +where smalltab.i = subq.i and smalltab.i = 123; + +-- Push down join quals. +explain (costs off) +select * from smalltab, +( + select bigtab.i, avg(bigtab.j) + from bigtab + group by bigtab.i +) as subq(i, avg) +where smalltab.i = subq.i; + +-- Subquery is LATERAL, and already references the other relation. The join +-- qual is always pushed down in that case, as the plan is "parameterized" +-- in respect to the other relation even if it was not pushed down. +explain (costs off) +select * from smalltab, +lateral ( + select bigtab.i, avg(bigtab.j) + from bigtab + where bigtab.j = smalltab.j + group by bigtab.i +) as subq(i, avg) +where smalltab.i < subq.i; + +-- Multiple join clauses constructed from equivalence classes +explain (costs off) +select * from smalltab, +lateral ( + select bigtab.i, bigtab.j, avg(bigtab.j) + from bigtab + where bigtab.j/2 = smalltab.j / 2 + group by bigtab.i, bigtab.j +) as subq(i, j, avg) +where smalltab.i = subq.i and smalltab.j = subq.j;