From 997e5c09ca017321d55fa292e09a327d8fabff1d Mon Sep 17 00:00:00 2001 From: reshke Date: Tue, 30 Jun 2026 06:30:26 +0000 Subject: [PATCH 1/5] JPPD guc --- src/backend/optimizer/path/costsize.c | 1 + src/backend/utils/misc/guc_parameters.dat | 8 ++++++++ src/include/optimizer/cost.h | 1 + 3 files changed, 10 insertions(+) diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 1c575e56ff..84dbace8a6 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; 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/optimizer/cost.h b/src/include/optimizer/cost.h index f2fd5d3150..854334f485 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; From ddc3cb321da058cd457bd453639a1f47da60375b Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 6 Jun 2018 15:58:49 +0300 Subject: [PATCH 2/5] Push down join quals into lateral subqueries. We don't normally push down join quals into subqueries, because that would require creating parameterized plans. However, if the plan is already parameterized because it's LATERAL, we might as well push down any additional join quals, that refer the same relations that are already referenced within the subquery. This changes the behavior of the sublevels_up parameters to ReplaceVarsFromTargetList(). The targetlist entries used to replace vars are no longer offset by that amount. I'm not sure what the original thinking on it was, but all the existing callers passed sublevels_up = 0, so I hope this is OK. Rebase-by: reshke --- src/backend/optimizer/path/allpaths.c | 194 +++++++++++++----- src/backend/optimizer/plan/createplan.c | 30 +++ src/backend/optimizer/prep/prepunion.c | 6 +- src/backend/optimizer/util/pathnode.c | 6 +- src/backend/rewrite/rewriteManip.c | 9 +- src/include/nodes/pathnodes.h | 1 + src/include/optimizer/pathnode.h | 3 +- .../regress/expected/subselect_pushdown.out | 91 ++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/subselect_pushdown.sql | 54 +++++ 10 files changed, 335 insertions(+), 61 deletions(-) create mode 100644 src/test/regress/expected/subselect_pushdown.out create mode 100644 src/test/regress/sql/subselect_pushdown.sql diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index c134594a21..fce0448b52 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -163,13 +163,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,6 +2673,11 @@ 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, @@ -2689,6 +2693,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Bitmapset *run_cond_attrs = NULL; ListCell *lc; char *plan_name; + List *pushed_down_ec_joins = NIL; /* * Must copy the Query so that planning doesn't mess up the RTE contents @@ -2699,8 +2704,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 +2743,140 @@ 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 && + if ((rel->baserestrictinfo != NIL || + (!bms_is_empty(required_outer) && (rel->joininfo || rel->has_eclass_joins))) && subquery_is_pushdown_safe(subquery, subquery, &safetyInfo)) { /* 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. + */ + 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, clause, &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; + + 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, clause, &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); @@ -2908,7 +2987,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_ec_joins)); } /* If outer rel allows parallelism, do same for partial paths. */ @@ -2935,7 +3014,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, create_subqueryscan_path(root, rel, subpath, trivial_pathtarget, pathkeys, - required_outer)); + required_outer, pushed_down_ec_joins)); } } } @@ -4505,6 +4584,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 +4629,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 +4643,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 +4684,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 +4693,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/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index de6a183da7..0e3878fc6e 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3544,6 +3544,9 @@ 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_ec_joins; /* it should be a subquery base rel... */ Assert(scan_relid > 0); @@ -3556,6 +3559,33 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path, */ subplan = create_plan(rel->subroot, best_path->subpath); + /* + * 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); diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index d1f022c5bf..f8446550bc 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -551,7 +551,7 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, subpath, trivial_tlist, pathkeys, - NULL)); + NULL, NIL)); } /* skip dealing with sorted paths if the setop doesn't need them */ @@ -619,7 +619,7 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, subpath, trivial_tlist, pathkeys, - NULL)); + NULL, NIL)); } } @@ -642,7 +642,7 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, partial_path = (Path *) create_subqueryscan_path(root, rel, 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..23a748734f 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -1908,7 +1908,7 @@ create_gather_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, SubqueryScanPath * create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, 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 +1923,7 @@ create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, pathnode->path.parallel_workers = subpath->parallel_workers; pathnode->path.pathkeys = pathkeys; pathnode->subpath = subpath; + pathnode->pushed_down_ec_joins = pushed_down_ec_joins; cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info, trivial_pathtarget); @@ -3979,7 +3980,8 @@ reparameterize_path(PlannerInfo *root, Path *path, subpath, trivial_pathtarget, spath->path.pathkeys, - required_outer); + required_outer, + spath->pushed_down_ec_joins); } case T_Result: /* Supported only for RTE_RESULT scan paths */ 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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 27a2c6815b..ed47d5af0b 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -2194,6 +2194,7 @@ typedef struct SubqueryScanPath { Path path; Path *subpath; /* path representing subquery execution */ + List *pushed_down_ec_joins; /* pushed-down quals derived from ECs */ } SubqueryScanPath; /* diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index e8db321f92..0729abadb9 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -139,7 +139,8 @@ extern SubqueryScanPath *create_subqueryscan_path(PlannerInfo *root, 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..f160bc84c8 --- /dev/null +++ b/src/test/regress/expected/subselect_pushdown.out @@ -0,0 +1,91 @@ +-- 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) + +-- Join quals are not currently pushed down +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 +------------------------------------------------- + Merge Join + Merge Cond: (smalltab.i = bigtab.i) + -> Sort + Sort Key: smalltab.i + -> Seq Scan on smalltab + -> GroupAggregate + Group Key: bigtab.i + -> Index Scan using bigtab_i on bigtab +(8 rows) + +-- Except when the subquery is LATERAL, and already references the other relation. +-- Such join clauses can be 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..712e1a82eb 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -76,7 +76,7 @@ test: brin_bloom brin_multi # ---------- # Another group of parallel tests # ---------- -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: 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 subselect_pushdown # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other # psql depends on create_am diff --git a/src/test/regress/sql/subselect_pushdown.sql b/src/test/regress/sql/subselect_pushdown.sql new file mode 100644 index 0000000000..c9e7a43794 --- /dev/null +++ b/src/test/regress/sql/subselect_pushdown.sql @@ -0,0 +1,54 @@ +-- 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; + +-- Join quals are not currently pushed down +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; + +-- Except when the subquery is LATERAL, and already references the other relation. +-- Such join clauses can be 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; From 73c269c2e2a7b93a40e91a343e4be508f73755ab Mon Sep 17 00:00:00 2001 From: reshke Date: Thu, 2 Jul 2026 06:50:34 +0000 Subject: [PATCH 3/5] fixes --- src/backend/optimizer/path/allpaths.c | 7 ++++--- src/test/regress/parallel_schedule | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index fce0448b52..c73ce07ed3 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -2815,10 +2815,11 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, } } - /* + /* * 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); @@ -2836,7 +2837,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, if (!rinfo->pseudoconstant && bms_is_subset(rinfo->required_relids, available_relids) && - qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo)) + qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo)) { /* Push it down */ subquery_push_qual(subquery, rte, rti, clause, 0); @@ -2866,7 +2867,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Node *clause = (Node *) rinfo->clause; if (!rinfo->pseudoconstant && - qual_is_pushdown_safe(subquery, rti, clause, &safetyInfo)) + qual_is_pushdown_safe(subquery, rti, rinfo, &safetyInfo)) { /* Push it down */ Assert(bms_is_subset(rinfo->required_relids, available_relids)); diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 712e1a82eb..ec70cd2532 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -76,7 +76,11 @@ test: brin_bloom brin_multi # ---------- # Another group of parallel tests # ---------- -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 subselect_pushdown +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 From 41a0881d6079a4e6a61bd6a944b8b3b414607149 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 6 Jun 2018 20:07:31 +0300 Subject: [PATCH 4/5] WIP: Allow pushing join quals down into subqueries. Plan subqueries a second time, to create parameterized plans with join quals. --- src/backend/optimizer/path/allpaths.c | 128 ++++++++++++++++-- src/backend/optimizer/path/costsize.c | 3 +- src/backend/optimizer/plan/createplan.c | 30 +++- src/backend/optimizer/plan/setrefs.c | 8 +- src/backend/optimizer/plan/subselect.c | 4 +- src/backend/optimizer/prep/prepunion.c | 8 +- src/backend/optimizer/util/pathnode.c | 13 +- src/backend/optimizer/util/relnode.c | 1 + src/backend/utils/adt/selfuncs.c | 19 ++- src/include/nodes/pathnodes.h | 7 +- src/include/optimizer/cost.h | 2 +- src/include/optimizer/pathnode.h | 2 + .../regress/expected/subselect_pushdown.out | 17 ++- src/test/regress/sql/subselect_pushdown.sql | 7 +- 14 files changed, 206 insertions(+), 43 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index c73ce07ed3..0fec55e535 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, @@ -2684,6 +2689,7 @@ 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; @@ -2694,6 +2700,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, 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 @@ -2755,9 +2762,10 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, * 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 || - (!bms_is_empty(required_outer) && (rel->joininfo || rel->has_eclass_joins))) && - 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 */ ListCell *l; @@ -2854,6 +2862,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, if (rel->has_eclass_joins) { List *clauses; + ListCell *lc; clauses = generate_join_implied_equalities(root, available_relids, @@ -2907,6 +2916,102 @@ 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(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); + 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(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.unsafeColumns); +} + +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; + /* plan_params should not be in use in current query level */ Assert(root->plan_params == NIL); @@ -2916,7 +3021,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, 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; /* @@ -2924,7 +3029,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)) { @@ -2936,8 +3041,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 @@ -2988,7 +3098,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, add_path(rel, (Path *) create_subqueryscan_path(root, rel, subpath, trivial_pathtarget, - pathkeys, required_outer, pushed_down_ec_joins)); + pathkeys, required_outer, pushed_down_clauses)); } /* If outer rel allows parallelism, do same for partial paths. */ @@ -3012,10 +3122,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, pushed_down_ec_joins)); + required_outer, pushed_down_clauses)); } } } diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 84dbace8a6..879d00968b 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -6045,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 0e3878fc6e..f5545dad80 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3546,18 +3546,40 @@ create_subqueryscan_plan(PlannerInfo *root, SubqueryScanPath *best_path, Plan *subplan; ListCell *l; List *qpqual; - List *sq_quals = best_path->pushed_down_ec_joins; + 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 @@ -3605,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); } @@ -3617,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 f8446550bc..b749bf0ed4 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -509,7 +509,7 @@ 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, rel->subroot); /* * Since we may want to add a partial path to this relation, we must set @@ -548,6 +548,8 @@ 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, @@ -616,6 +618,8 @@ 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, @@ -640,7 +644,7 @@ 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); add_partial_path(rel, partial_path); diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 23a748734f..913082795d 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -1906,7 +1906,10 @@ 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 *pushed_down_ec_joins) { @@ -1923,7 +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->pushed_down_ec_joins = pushed_down_ec_joins; + 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); @@ -3978,10 +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, - spath->pushed_down_ec_joins); + 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/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/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ed47d5af0b..814b0a33bb 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,7 +2193,9 @@ typedef struct SubqueryScanPath { Path path; Path *subpath; /* path representing subquery execution */ - List *pushed_down_ec_joins; /* pushed-down quals derived from ECs */ + PlannerInfo *subroot; /* */ + List *subplan_params; /* */ + List *pushed_down_ec_joins; /* pushed-down quals */ } SubqueryScanPath; /* diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index 854334f485..e516bee612 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -209,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 0729abadb9..d779ea4cbe 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -136,6 +136,8 @@ 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, diff --git a/src/test/regress/expected/subselect_pushdown.out b/src/test/regress/expected/subselect_pushdown.out index f160bc84c8..792f66e7fd 100644 --- a/src/test/regress/expected/subselect_pushdown.out +++ b/src/test/regress/expected/subselect_pushdown.out @@ -25,7 +25,7 @@ where smalltab.i = subq.i and smalltab.i = 123; Index Cond: (i = 123) (7 rows) --- Join quals are not currently pushed down +-- Push down join quals. explain (costs off) select * from smalltab, ( @@ -36,18 +36,17 @@ select * from smalltab, where smalltab.i = subq.i; QUERY PLAN ------------------------------------------------- - Merge Join - Merge Cond: (smalltab.i = bigtab.i) - -> Sort - Sort Key: smalltab.i - -> Seq Scan on smalltab + Nested Loop + -> Seq Scan on smalltab -> GroupAggregate Group Key: bigtab.i -> Index Scan using bigtab_i on bigtab -(8 rows) + Index Cond: (smalltab.i = i) +(6 rows) --- Except when the subquery is LATERAL, and already references the other relation. --- Such join clauses can be pushed down. +-- 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 ( diff --git a/src/test/regress/sql/subselect_pushdown.sql b/src/test/regress/sql/subselect_pushdown.sql index c9e7a43794..d6431d314c 100644 --- a/src/test/regress/sql/subselect_pushdown.sql +++ b/src/test/regress/sql/subselect_pushdown.sql @@ -20,7 +20,7 @@ select * from smalltab, ) as subq(i, avg) where smalltab.i = subq.i and smalltab.i = 123; --- Join quals are not currently pushed down +-- Push down join quals. explain (costs off) select * from smalltab, ( @@ -30,8 +30,9 @@ select * from smalltab, ) as subq(i, avg) where smalltab.i = subq.i; --- Except when the subquery is LATERAL, and already references the other relation. --- Such join clauses can be pushed down. +-- 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 ( From da1b42cf688f5af228a6dd8ed946b09feddec6ef Mon Sep 17 00:00:00 2001 From: reshke Date: Thu, 16 Jul 2026 18:58:08 +0000 Subject: [PATCH 5/5] fix2 --- src/backend/optimizer/path/allpaths.c | 13 ++++++++----- src/backend/optimizer/prep/prepunion.c | 8 ++++---- src/include/nodes/pathnodes.h | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 0fec55e535..4b1c547767 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -2953,7 +2953,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, { /* Push it down */ required_outer = bms_union(required_outer, - pull_varnos(clause)); + pull_varnos(root, clause)); required_outer = bms_del_member(required_outer, rti); subquery_push_qual(subquery, rte, rti, clause, 0); @@ -2972,7 +2972,8 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, clauses = generate_join_implied_equalities(root, available_relids, other_relids, - rel); + rel, + NULL); foreach(lc, clauses) { RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); @@ -2983,7 +2984,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, { /* Push it down */ required_outer = bms_union(required_outer, - pull_varnos(clause)); + pull_varnos(root, clause)); required_outer = bms_del_member(required_outer, rti); subquery_push_qual(subquery, rte, rti, clause, 0); @@ -2997,7 +2998,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, subquery, pushed_down_clauses, tuple_fraction, false); } - pfree(safetyInfo.unsafeColumns); + pfree(safetyInfo.unsafeFlags); } static void @@ -3011,13 +3012,15 @@ add_subqueryscan_variant(PlannerInfo *root, RelOptInfo *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 */ diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index b749bf0ed4..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, rel->subroot); + 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 */ diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 814b0a33bb..dd7a00ce9c 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -2195,7 +2195,7 @@ typedef struct SubqueryScanPath Path *subpath; /* path representing subquery execution */ PlannerInfo *subroot; /* */ List *subplan_params; /* */ - List *pushed_down_ec_joins; /* pushed-down quals */ + List *pushed_down_joins; /* pushed-down quals */ } SubqueryScanPath; /*