From e7b8ad7ed9653d237e09dee89ba419585717a789 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Tue, 3 Nov 2026 21:50:41 -0500 Subject: [PATCH v1 2/2] Convert pathkeys of nested set operations instead of sharing classes A child set operation keeps its own pathkeys. The parent converts them to its output columns, like convert_subquery_pathkeys(). Discussion: https://postgr.es/m/1087303.1791221946@sss.pgh.pa.us --- src/backend/optimizer/path/equivclass.c | 164 +++++++---------------- src/backend/optimizer/prep/prepunion.c | 166 ++++++++++++++++++++---- src/include/optimizer/paths.h | 1 - src/test/regress/expected/union.out | 51 ++++++++ src/test/regress/sql/union.sql | 11 ++ 5 files changed, 250 insertions(+), 143 deletions(-) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 9888ad89322..ddcfbcd5ba6 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -30,6 +30,7 @@ #include "optimizer/paths.h" #include "optimizer/planmain.h" #include "optimizer/restrictinfo.h" +#include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" @@ -747,6 +748,7 @@ get_eclass_for_sort_expr(PlannerInfo *root, Relids expr_relids; EquivalenceClass *newec; EquivalenceMember *newem; + Index setop_varno = 0; ListCell *lc1; MemoryContext oldcontext; @@ -761,6 +763,26 @@ get_eclass_for_sort_expr(PlannerInfo *root, */ jdomain = linitial_node(JoinDomain, root->join_domains); + /* + * A Var of an RTE_SETOP entry can only match a parent member of an EC + * whose ec_relids include its varno, since parent members get their + * relids from pull_varnos and ec_relids never shrink. Checking that + * first is much cheaper than comparing opfamilies and members. + */ + if (relids == NULL) + { + Expr *e = expr; + + while (IsA(e, RelabelType)) + e = ((RelabelType *) e)->arg; + if (IsA(e, Var) && + ((Var *) e)->varlevelsup == 0 && + ((Var *) e)->varnullingrels == NULL && + !IS_SPECIAL_VARNO(((Var *) e)->varno) && + planner_rt_fetch(((Var *) e)->varno, root)->rtekind == RTE_SETOP) + setop_varno = ((Var *) e)->varno; + } + /* * Scan through the existing EquivalenceClasses for a match */ @@ -770,6 +792,9 @@ get_eclass_for_sort_expr(PlannerInfo *root, EquivalenceMemberIterator it; EquivalenceMember *cur_em; + if (setop_varno > 0 && !bms_is_member(setop_varno, cur_ec->ec_relids)) + continue; + /* * Never match to a volatile EC, except when we are looking at another * reference to the same volatile SortGroupClause. @@ -3149,7 +3174,8 @@ add_child_rel_pathkey_equivalences(PlannerInfo *root, RelOptInfo *child_rel, * * 'root' is the PlannerInfo belonging to the top-level set operation. * 'child_rel' is the RelOptInfo of the child relation we're adding - * EquivalenceMembers for. + * EquivalenceMembers for. This is either a subquery's base relation or the + * upper relation of a set operation nested below the one being planned. * 'child_tlist' is the target list for the setop child relation. The target * list expressions are what we add as EquivalenceMembers. * 'setop_pathkeys' is a list of PathKeys which must contain an entry for each @@ -3161,6 +3187,16 @@ add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, { ListCell *lc; ListCell *lc2 = list_head(setop_pathkeys); + Index child_relid; + + /* + * A nested set operation has no relid of its own, so store its members + * under the lowest relid in its relids, as is done for child joinrels. + */ + if (child_rel->reloptkind == RELOPT_BASEREL) + child_relid = child_rel->relid; + else + child_relid = bms_next_member(child_rel->relids, -1); foreach(lc, child_tlist) { @@ -3183,6 +3219,11 @@ add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, * likewise, the JoinDomain can be that of the initial member of the * Pathkey's EquivalenceClass. We pass -1 for ec_index since we * maintain the eclass_indexes for the child_rel after the loop. + * + * The child member takes its parent's em_datatype, not the type of + * the child expression. The two differ when the column type is only + * binary-compatible with the opclass input type, such as varchar with + * text_ops, and a sort operator can only be looked up by the latter. */ add_child_eq_member(root, pk->pk_eclass, @@ -3191,8 +3232,8 @@ add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, child_rel->relids, parent_em->em_jdomain, parent_em, - exprType((Node *) tle->expr), - child_rel->relid); + parent_em->em_datatype, + child_relid); lc2 = lnext(setop_pathkeys, lc2); } @@ -3207,123 +3248,6 @@ add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, list_length(root->eq_classes) - 1); } -/* - * add_setop_output_equivalences - * Add the output Vars of a set operation to the EquivalenceClasses that - * already contain the same output column of another set operation in - * this query level. - * - * Nested set operations are planned within one PlannerInfo. A parent can - * only make use of a child set operation's sorted paths if both refer to - * their output columns through the same EquivalenceClasses, so whenever a - * Var in 'tlist' matches the RTE_SETOP Var of another set operation's output - * column (same column number, type, typmod and collation), we add it to each - * EquivalenceClass containing that Var. This must be done before building - * any pathkeys from 'tlist'. Entries of 'tlist' that are not RTE_SETOP Vars - * are ignored. - */ -void -add_setop_output_equivalences(PlannerInfo *root, List *tlist) -{ - int ncols = list_length(tlist); - Var **vars; - EquivalenceMember **match; - bool *present; - bool found = false; - - /* Index our output Vars by column number */ - vars = palloc0_array(Var *, ncols + 1); - foreach_node(TargetEntry, tle, tlist) - { - Var *var = (Var *) tle->expr; - - /* Only output columns of set operations are of interest */ - if (!IsA(var, Var) || var->varlevelsup != 0 || - var->varattno < 1 || var->varattno > ncols || - root->simple_rte_array[var->varno]->rtekind != RTE_SETOP) - continue; - /* Callers build 'tlist' so that each column number appears once */ - Assert(vars[var->varattno] == NULL); - vars[var->varattno] = var; - found = true; - } - if (!found) - return; - - match = palloc0_array(EquivalenceMember *, ncols + 1); - present = palloc0_array(bool, ncols + 1); - - foreach_node(EquivalenceClass, ec, root->eq_classes) - { - List *seen = NIL; - - if (ec->ec_has_volatile) - continue; - - /* - * For each of our columns, find the first member that is the same - * output column of another set operation, and note whether our own - * Var is already a member. - */ - foreach_node(EquivalenceMember, em, ec->ec_members) - { - Expr *expr = em->em_expr; - Var *emvar; - Var *var; - - /* Look through any RelabelType added by canonicalization */ - while (IsA(expr, RelabelType)) - expr = ((RelabelType *) expr)->arg; - if (!IsA(expr, Var)) - continue; - emvar = (Var *) expr; - if (emvar->varlevelsup != 0 || - emvar->varattno < 1 || emvar->varattno > ncols) - continue; - var = vars[emvar->varattno]; - if (var == NULL || - root->simple_rte_array[emvar->varno]->rtekind != RTE_SETOP || - emvar->vartype != var->vartype || - emvar->vartypmod != var->vartypmod || - emvar->varcollid != var->varcollid) - continue; - - if (match[var->varattno] == NULL && !present[var->varattno]) - seen = lappend_int(seen, var->varattno); - if (emvar->varno == var->varno) - present[var->varattno] = true; - else if (match[var->varattno] == NULL) - match[var->varattno] = em; - } - - /* Add a copy of each match that uses our varno, then reset */ - foreach_int(attno, seen) - { - if (match[attno] != NULL && !present[attno]) - { - Expr *newexpr = copyObject(match[attno]->em_expr); - Expr *expr = newexpr; - - while (IsA(expr, RelabelType)) - expr = ((RelabelType *) expr)->arg; - ((Var *) expr)->varno = vars[attno]->varno; - - add_eq_member(ec, newexpr, - bms_make_singleton(vars[attno]->varno), - match[attno]->em_jdomain, - match[attno]->em_datatype); - } - match[attno] = NULL; - present[attno] = false; - } - list_free(seen); - } - - pfree(vars); - pfree(match); - pfree(present); -} - /* * setup_eclass_member_iterator * Setup an EquivalenceMemberIterator 'it' to iterate over all parent diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index 3d0a5e25303..45a97fd1e46 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -685,6 +685,128 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, } } +/* + * convert_setop_child_path + * Return a path of a nested set operation, with its pathkeys + * converted to refer to the output columns of the parent set operation. + * + * The paths of a set operation nested below the one being planned are + * sorted by the child's output Vars, which the parent knows nothing about. + * Column k of the parent has the same value as column k of the child if the + * child's column is a plain output Var of the same type, typmod and + * collation, and in that case a pathkey for the child's column can be + * converted to the parent's. We stop at the first pathkey that cannot be + * converted. + * + * 'child_tlist' is the child's target list as seen by the parent, and + * 'parent_tlist' is the parent's. The path is wrapped in a + * ProjectionPath that computes the same target, so it costs the same as + * the original and does not add a node to the plan. + */ +static Path * +convert_setop_child_path(PlannerInfo *root, Path *path, + List *child_tlist, List *parent_tlist) +{ + List *pathkeys = NIL; + ProjectionPath *ppath; + + Assert(list_length(child_tlist) == list_length(parent_tlist)); + + foreach_node(PathKey, pk, path->pathkeys) + { + EquivalenceClass *ec = pk->pk_eclass; + EquivalenceClass *parent_ec = NULL; + + foreach_node(EquivalenceMember, em, ec->ec_members) + { + Expr *expr = em->em_expr; + Var *var; + Var *parent_var; + + /* Look through any RelabelType added by canonicalization */ + while (IsA(expr, RelabelType)) + expr = ((RelabelType *) expr)->arg; + if (!IsA(expr, Var)) + continue; + var = (Var *) expr; + if (var->varlevelsup != 0 || var->varattno < 1 || + var->varattno > list_length(child_tlist)) + continue; + + /* It must be the child's output column ... */ + if (!equal(list_nth_node(TargetEntry, child_tlist, + var->varattno - 1)->expr, var)) + continue; + + /* ... with the same type, typmod and collation as ours */ + parent_var = (Var *) list_nth_node(TargetEntry, parent_tlist, + var->varattno - 1)->expr; + if (!IsA(parent_var, Var) || + parent_var->vartype != var->vartype || + parent_var->vartypmod != var->vartypmod || + parent_var->varcollid != var->varcollid) + continue; + + parent_ec = get_eclass_for_sort_expr(root, (Expr *) parent_var, + ec->ec_opfamilies, + em->em_datatype, + ec->ec_collation, + 0, NULL, false); + if (parent_ec != NULL) + break; + } + + if (parent_ec == NULL) + break; + + pathkeys = lappend(pathkeys, + make_canonical_pathkey(root, parent_ec, + pk->pk_opfamily, + pk->pk_cmptype, + pk->pk_nulls_first)); + } + + ppath = create_projection_path(root, path->parent, path, path->pathtarget); + ppath->path.pathkeys = pathkeys; + + return (Path *) ppath; +} + +/* + * get_setop_child_sorted_path + * Find the cheapest path of a nested set operation 'rel' that is sorted + * by the parent's 'pathkeys', or return NULL if there is none. + * + * The result is converted by convert_setop_child_path(). We also add the + * child's output columns as child members of the parent's + * EquivalenceClasses, so that createplan.c can find the sort columns in the + * child's target list. + */ +static Path * +get_setop_child_sorted_path(PlannerInfo *root, RelOptInfo *rel, + List *child_tlist, List *parent_tlist, + List *pathkeys) +{ + List *paths = NIL; + Path *result; + + foreach_ptr(Path, path, rel->pathlist) + { + if (path->pathkeys != NIL) + paths = lappend(paths, convert_setop_child_path(root, path, + child_tlist, + parent_tlist)); + } + + result = get_cheapest_path_for_pathkeys(paths, pathkeys, NULL, + TOTAL_COST, false); + if (result != NULL) + add_setop_child_rel_equivalences(root, rel, child_tlist, pathkeys); + + list_free(paths); + return result; +} + /* * Generate paths for a UNION or UNION ALL node */ @@ -738,9 +860,6 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, tlist_list, refnames_tlist); *pTargetList = tlist; - /* Share pathkeys with any set operations nested below this one */ - add_setop_output_equivalences(root, tlist); - /* For UNIONs (not UNION ALL), try sorting, if sorting is possible */ if (!op->all) { @@ -790,9 +909,10 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, } /* Build path lists and relid set. */ - foreach(lc, rellist) + forboth(lc, rellist, lc3, tlist_list) { RelOptInfo *rel = lfirst(lc); + List *child_tlist = lfirst_node(List, lc3); Path *ordered_path; /* @@ -810,11 +930,16 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, if (try_sorted) { - ordered_path = get_cheapest_path_for_pathkeys(rel->pathlist, - union_pathkeys, - NULL, - TOTAL_COST, - false); + if (rel->rtekind == RTE_SUBQUERY) + ordered_path = get_cheapest_path_for_pathkeys(rel->pathlist, + union_pathkeys, + NULL, + TOTAL_COST, + false); + else + ordered_path = get_setop_child_sorted_path(root, rel, + child_tlist, tlist, + union_pathkeys); if (ordered_path != NULL) ordered.subpaths = lappend(ordered.subpaths, ordered_path); @@ -1124,9 +1249,6 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, *pTargetList = tlist; - /* Share pathkeys with any set operations nested below this one */ - add_setop_output_equivalences(root, tlist); - /* Identify the grouping semantics */ groupList = generate_setop_grouplist(op, tlist); @@ -1148,14 +1270,6 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, tlist); root->query_pathkeys = nonunion_pathkeys; - - /* - * A child set operation that built no pathkeys of its own (such as - * UNION ALL) has not been linked to our EquivalenceClasses yet. Do - * it now, so that sorting a child's output yields our pathkeys. - */ - add_setop_output_equivalences(root, lpath_tlist); - add_setop_output_equivalences(root, rpath_tlist); } /* @@ -1359,8 +1473,10 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, slpath = lpath; /* cheapest path is already sorted */ else { + /* A nested set operation's paths are sorted by its own columns */ slpath = get_cheapest_path_for_pathkeys(lrel->pathlist, - nonunion_pathkeys, + lrel->rtekind == RTE_SUBQUERY ? + nonunion_pathkeys : pathkeys, NULL, TOTAL_COST, false); @@ -1372,6 +1488,8 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, pathkeys, -1.0); } + if (lrel->rtekind != RTE_SUBQUERY) + slpath = convert_setop_child_path(root, slpath, lpath_tlist, tlist); /* and now the same for the right. */ pathkeys = make_pathkeys_for_sortclauses(root, @@ -1381,8 +1499,10 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, srpath = rpath; /* cheapest path is already sorted */ else { + /* A nested set operation's paths are sorted by its own columns */ srpath = get_cheapest_path_for_pathkeys(rrel->pathlist, - nonunion_pathkeys, + rrel->rtekind == RTE_SUBQUERY ? + nonunion_pathkeys : pathkeys, NULL, TOTAL_COST, false); @@ -1394,6 +1514,8 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, pathkeys, -1.0); } + if (rrel->rtekind != RTE_SUBQUERY) + srpath = convert_setop_child_path(root, srpath, rpath_tlist, tlist); path = (Path *) create_setop_path(root, result_rel, diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index eac439578c6..64f87da1674 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -191,7 +191,6 @@ extern void add_setop_child_rel_equivalences(PlannerInfo *root, RelOptInfo *child_rel, List *child_tlist, List *setop_pathkeys); -extern void add_setop_output_equivalences(PlannerInfo *root, List *tlist); extern void setup_eclass_member_iterator(EquivalenceMemberIterator *it, EquivalenceClass *ec, Relids child_relids); diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index 9cffe854b19..8cbca3eae2f 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -1598,6 +1598,57 @@ ORDER BY 1; Index Cond: (unique1 < 5) (8 rows) +-- Same, with a type that is only binary-compatible with the input type of +-- its sort opclass. The Merge Append must still find a sort operator. +EXPLAIN (COSTS OFF) +(SELECT stringu1::varchar FROM tenk1 WHERE unique1 < 3 + UNION SELECT stringu2::varchar FROM tenk1 WHERE unique1 < 3 + EXCEPT SELECT stringu1::varchar FROM tenk1 WHERE unique1 = 0) +UNION +(SELECT stringu1::varchar FROM tenk1 WHERE unique1 BETWEEN 5 AND 6 + UNION SELECT stringu2::varchar FROM tenk1 WHERE unique1 BETWEEN 5 AND 6 + EXCEPT SELECT stringu2::varchar FROM tenk1 WHERE unique1 = 5); + QUERY PLAN +--------------------------------------------------------------------------------------------- + Unique + -> Merge Append + Sort Key: ((tenk1.stringu1)::character varying) COLLATE "C" + -> SetOp Except + -> Unique + -> Sort + Sort Key: ((tenk1.stringu1)::character varying) COLLATE "C" + -> Append + -> Bitmap Heap Scan on tenk1 + Recheck Cond: (unique1 < 3) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 < 3) + -> Bitmap Heap Scan on tenk1 tenk1_1 + Recheck Cond: (unique1 < 3) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (unique1 < 3) + -> Sort + Sort Key: ((tenk1_2.stringu1)::character varying) COLLATE "C" + -> Index Scan using tenk1_unique1 on tenk1 tenk1_2 + Index Cond: (unique1 = 0) + -> SetOp Except + -> Unique + -> Sort + Sort Key: ((tenk1_3.stringu1)::character varying) COLLATE "C" + -> Append + -> Bitmap Heap Scan on tenk1 tenk1_3 + Recheck Cond: ((unique1 >= 5) AND (unique1 <= 6)) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: ((unique1 >= 5) AND (unique1 <= 6)) + -> Bitmap Heap Scan on tenk1 tenk1_4 + Recheck Cond: ((unique1 >= 5) AND (unique1 <= 6)) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: ((unique1 >= 5) AND (unique1 <= 6)) + -> Sort + Sort Key: ((tenk1_5.stringu2)::character varying) COLLATE "C" + -> Index Scan using tenk1_unique1 on tenk1 tenk1_5 + Index Cond: (unique1 = 5) +(37 rows) + RESET enable_hashagg; -- Test constraint exclusion of UNION ALL subqueries explain (costs off) diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql index 59e376b73a4..6b1aa18b075 100644 --- a/src/test/regress/sql/union.sql +++ b/src/test/regress/sql/union.sql @@ -611,6 +611,17 @@ EXPLAIN (COSTS OFF) EXCEPT SELECT unique1 FROM tenk1 WHERE unique1 < 5 ORDER BY 1; + +-- Same, with a type that is only binary-compatible with the input type of +-- its sort opclass. The Merge Append must still find a sort operator. +EXPLAIN (COSTS OFF) +(SELECT stringu1::varchar FROM tenk1 WHERE unique1 < 3 + UNION SELECT stringu2::varchar FROM tenk1 WHERE unique1 < 3 + EXCEPT SELECT stringu1::varchar FROM tenk1 WHERE unique1 = 0) +UNION +(SELECT stringu1::varchar FROM tenk1 WHERE unique1 BETWEEN 5 AND 6 + UNION SELECT stringu2::varchar FROM tenk1 WHERE unique1 BETWEEN 5 AND 6 + EXCEPT SELECT stringu2::varchar FROM tenk1 WHERE unique1 = 5); RESET enable_hashagg; -- Test constraint exclusion of UNION ALL subqueries -- 2.37.1 (Apple Git-137.1)