From 2e4ae7f907fe4f65b33747f87521d5b39dc13b83 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Sun, 1 Nov 2026 23:58:17 -0500 Subject: [PATCH v1 1/2] Represent set operation outputs with RTE_SETOP range table entries Give each set operation its own RTE_SETOP range table entry, so its output Vars no longer use varno 0. Discussion: https://postgr.es/m/1087303.1791221946@sss.pgh.pa.us --- .../expected/pg_overexplain.out | 33 +++++ contrib/pg_overexplain/pg_overexplain.c | 3 + contrib/pg_overexplain/sql/pg_overexplain.sql | 4 + src/backend/nodes/nodeFuncs.c | 2 + src/backend/nodes/outfuncs.c | 5 + src/backend/nodes/print.c | 4 + src/backend/nodes/readfuncs.c | 5 + src/backend/optimizer/path/allpaths.c | 1 + src/backend/optimizer/path/costsize.c | 28 ++-- src/backend/optimizer/path/equivclass.c | 122 +++++++++++++++++- src/backend/optimizer/plan/setrefs.c | 55 +++++--- src/backend/optimizer/prep/prepjointree.c | 10 +- src/backend/optimizer/prep/prepunion.c | 69 ++++++---- src/backend/parser/analyze.c | 48 +++++++ src/backend/parser/parse_relation.c | 53 +++++++- src/backend/parser/parse_target.c | 10 ++ src/backend/rewrite/rewriteManip.c | 34 ++++- src/backend/rewrite/rewriteSearchCycle.c | 23 +++- src/backend/utils/adt/ruleutils.c | 8 ++ src/backend/utils/adt/selfuncs.c | 19 +-- src/include/nodes/parsenodes.h | 18 ++- src/include/optimizer/paths.h | 1 + src/include/parser/parse_relation.h | 5 + .../regress/expected/incremental_sort.out | 2 +- src/test/regress/expected/union.out | 115 ++++++++++++++++- src/test/regress/sql/incremental_sort.sql | 2 +- src/test/regress/sql/union.sql | 48 ++++++- src/tools/pgindent/typedefs.list | 1 + 28 files changed, 649 insertions(+), 79 deletions(-) diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out index c4857ed80be..b72c25b7a13 100644 --- a/contrib/pg_overexplain/expected/pg_overexplain.out +++ b/contrib/pg_overexplain/expected/pg_overexplain.out @@ -844,3 +844,36 @@ SELECT * FROM (SELECT * FROM creatures OFFSET 0) ss; Unprunable RTIs: 2 3 4 5 6 (50 rows) +-- test display of the RTE_SETOP entry for a set operation +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT 1 AS a UNION SELECT 2 AS b; + QUERY PLAN +---------------------------------------------------- + Unique + -> Sort + Sort Key: (1) + -> Append + Append RTIs: 1 2 + Child Append RTIs: none + -> Result + RTIs: 4 + Elided Node Type: SubqueryScan + Elided Node RTIs: 1 + -> Result + RTIs: 5 + Elided Node Type: SubqueryScan + Elided Node RTIs: 2 + RTI 1 (subquery): + Eref: unnamed_subquery (a) + RTI 2 (subquery): + Eref: unnamed_subquery (b) + RTI 3 (setop): + Eref: "*SETOP*" (a) + RTI 4 (result): + Subplan: setop_1 + Eref: "*RESULT*" () + RTI 5 (result): + Subplan: setop_2 + Eref: "*RESULT*" () +(26 rows) + diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c index d71a59d0ac2..25b92cf4627 100644 --- a/contrib/pg_overexplain/pg_overexplain.c +++ b/contrib/pg_overexplain/pg_overexplain.c @@ -524,6 +524,9 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) case RTE_GROUP: kind = "group"; break; + case RTE_SETOP: + kind = "setop"; + break; } /* Begin group for this specific RTE */ diff --git a/contrib/pg_overexplain/sql/pg_overexplain.sql b/contrib/pg_overexplain/sql/pg_overexplain.sql index ac71da3012a..2396b77de74 100644 --- a/contrib/pg_overexplain/sql/pg_overexplain.sql +++ b/contrib/pg_overexplain/sql/pg_overexplain.sql @@ -159,3 +159,7 @@ SELECT * FROM vegetables v, -- test display of child append RTIs EXPLAIN (RANGE_TABLE, COSTS OFF) SELECT * FROM (SELECT * FROM creatures OFFSET 0) ss; + +-- test display of the RTE_SETOP entry for a set operation +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT 1 AS a UNION SELECT 2 AS b; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 678b9a046ce..56ccc715333 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2877,6 +2877,7 @@ range_table_entry_walker_impl(RangeTblEntry *rte, case RTE_CTE: case RTE_NAMEDTUPLESTORE: case RTE_RESULT: + case RTE_SETOP: /* nothing to do */ break; case RTE_GROUP: @@ -3932,6 +3933,7 @@ range_table_mutator_impl(List *rtable, case RTE_CTE: case RTE_NAMEDTUPLESTORE: case RTE_RESULT: + case RTE_SETOP: /* nothing to do */ break; case RTE_GROUP: diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 40990143927..d3045b667e8 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -571,6 +571,11 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) case RTE_GROUP: WRITE_NODE_FIELD(groupexprs); break; + case RTE_SETOP: + WRITE_NODE_FIELD(coltypes); + WRITE_NODE_FIELD(coltypmods); + WRITE_NODE_FIELD(colcollations); + break; default: elog(ERROR, "unrecognized RTE kind: %d", (int) node->rtekind); break; diff --git a/src/backend/nodes/print.c b/src/backend/nodes/print.c index 12f2b95d87c..d8822d1752e 100644 --- a/src/backend/nodes/print.c +++ b/src/backend/nodes/print.c @@ -304,6 +304,10 @@ print_rt(const List *rtable) printf("%d\t%s\t[group]", i, rte->eref->aliasname); break; + case RTE_SETOP: + printf("%d\t%s\t[setop]", + i, rte->eref->aliasname); + break; default: printf("%d\t%s\t[unknown rtekind]", i, rte->eref->aliasname); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 2839a711f9e..78f5d7a2d21 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -429,6 +429,11 @@ _readRangeTblEntry(ReadNodeContext *ctx) case RTE_GROUP: READ_NODE_FIELD(groupexprs); break; + case RTE_SETOP: + READ_NODE_FIELD(coltypes); + READ_NODE_FIELD(coltypmods); + READ_NODE_FIELD(colcollations); + break; default: elog(ERROR, "unrecognized RTE kind: %d", (int) local_node->rtekind); diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 72ad61e8c24..05410890756 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -792,6 +792,7 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel, /* RESULT RTEs, in themselves, are no problem. */ break; case RTE_GROUP: + case RTE_SETOP: /* Shouldn't happen; we're only considering baserels here. */ Assert(false); return; diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 7bbddb8bee4..0f47911d9b8 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -2089,15 +2089,16 @@ cost_incremental_sort(Path *path, /* * Extract presorted keys as list of expressions. * - * We need to be careful about Vars containing "varno 0" which might have - * been introduced by generate_append_tlist, which would confuse - * estimate_num_groups (in fact it'd fail for such expressions). See + * We need to be careful about Vars referencing an RTE_SETOP entry, which + * might have been introduced by generate_append_tlist, and which would + * confuse estimate_num_groups (in fact it'd fail for such expressions, + * since there is no RelOptInfo for such an entry). See * recurse_set_operations which has to deal with the same issue. * * Unlike recurse_set_operations we can't access the original target list * here, and even if we could it's not very clear how useful would that be * for a set operation combining multiple tables. So we simply detect if - * there are any expressions with "varno 0" and use the default + * there are any expressions with RTE_SETOP Vars and use the default * DEFAULT_NUM_DISTINCT in that case. * * We might also use either 1.0 (a single group) or input_tuples (each row @@ -2113,18 +2114,25 @@ cost_incremental_sort(Path *path, PathKey *key = (PathKey *) lfirst(l); EquivalenceMember *member = (EquivalenceMember *) linitial(key->pk_eclass->ec_members); + Relids varnos = pull_varnos(root, (Node *) member->em_expr); + int varno = -1; /* - * Check if the expression contains Var with "varno 0" so that we - * don't call estimate_num_groups in that case. + * Check if the expression contains a Var referencing an RTE_SETOP + * entry so that we don't call estimate_num_groups in that case. */ - if (bms_is_member(0, pull_varnos(root, (Node *) member->em_expr))) + while ((varno = bms_next_member(varnos, varno)) >= 0) { - unknown_varno = true; - break; + if (planner_rt_fetch(varno, root)->rtekind == RTE_SETOP) + { + unknown_varno = true; + break; + } } + if (unknown_varno) + break; - /* expression not containing any Vars with "varno 0" */ + /* expression not containing any RTE_SETOP Vars */ presortedExprs = lappend(presortedExprs, member->em_expr); if (foreach_current_index(l) + 1 >= presorted_keys) diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 598b62b6764..9888ad89322 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -883,9 +883,10 @@ get_eclass_for_sort_expr(PlannerInfo *root, if (i == root->group_rtindex) continue; - if (rel == NULL) /* must be an outer join */ + if (rel == NULL) /* must be an outer join or a set operation */ { - Assert(bms_is_member(i, root->outer_join_rels)); + Assert(bms_is_member(i, root->outer_join_rels) || + root->simple_rte_array[i]->rtekind == RTE_SETOP); continue; } @@ -3206,6 +3207,123 @@ 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/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index af78892fafc..164f2ae8e70 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -26,6 +26,7 @@ #include "optimizer/subselect.h" #include "optimizer/tlist.h" #include "parser/parse_relation.h" +#include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "tcop/utility.h" #include "utils/syscache.h" @@ -95,6 +96,13 @@ typedef struct Query *query; } flatten_rtes_walker_context; +/* Context info for fix_dummy_setop_vars_mutator() */ +typedef struct +{ + PlannerInfo *root; + int first_child_relid; +} fix_dummy_setop_vars_context; + /* * Selecting the best alternative in an AlternativeSubPlan expression requires * estimating how many times that expression will be evaluated. For an @@ -156,7 +164,8 @@ static Plan *set_mergeappend_references(PlannerInfo *root, static void set_hash_references(PlannerInfo *root, Plan *plan, int rtoffset); static Relids offset_relid_set(Relids relids, int rtoffset); static List *offset_relid_set_list(List *relid_sets, int rtoffset); -static Node *fix_dummy_setop_vars_mutator(Node *node, int *first_child_relid); +static Node *fix_dummy_setop_vars_mutator(Node *node, + fix_dummy_setop_vars_context *context); static Node *fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec); static Node *fix_scan_expr_mutator(Node *node, fix_scan_expr_context *context); @@ -1044,7 +1053,7 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) set_upper_references(root, plan, rtoffset); else { - int first_child_relid; + fix_dummy_setop_vars_context dsv_context; /* * The tlist of a childless Result could contain @@ -1060,21 +1069,24 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) * * We also must handle the case where set operations have * been proven empty, resulting in a dummy Result node. - * Because prepunion.c uses varno 0 for setop targetlists, - * that's what we'll find here. Replace such Vars with - * Vars pointing at the Result's lowest-numbered replaced - * rel, which will be its leftmost set-op child. While we - * can assume that ROWID_VARs are at top level, varno 0 - * Vars might be buried in coercion expressions, so that - * needs a recursive traversal. Without this, EXPLAIN - * will have trouble displaying targetlists of dummy set + * Because prepunion.c makes setop targetlists out of Vars + * that reference RTE_SETOP entries, that's what we'll + * find here. Replace such Vars with Vars pointing at the + * Result's lowest-numbered replaced rel, which will be + * its leftmost set-op child. While we can assume that + * ROWID_VARs are at top level, RTE_SETOP Vars might be + * buried in coercion expressions, so that needs a + * recursive traversal. Without this, EXPLAIN would show + * the RTE_SETOP entry's name in targetlists of dummy set * operations. * * Note that some Results have empty relids, leading to * first_child_relid being negative. We assume such - * Results can't contain any varno 0 Vars. + * Results can't contain any RTE_SETOP Vars. */ - first_child_relid = bms_next_member(splan->relids, -1); + dsv_context.root = root; + dsv_context.first_child_relid = + bms_next_member(splan->relids, -1); foreach(l, splan->plan.targetlist) { TargetEntry *tle = (TargetEntry *) lfirst(l); @@ -1084,10 +1096,10 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) tle->expr = (Expr *) makeNullConst(var->vartype, var->vartypmod, var->varcollid); - else if (first_child_relid > 0) + else if (dsv_context.first_child_relid > 0) tle->expr = (Expr *) fix_dummy_setop_vars_mutator((Node *) tle->expr, - &first_child_relid); + &dsv_context); } splan->plan.targetlist = @@ -2282,10 +2294,14 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, /* * fix_dummy_setop_vars_mutator - * Change the varno 0 Vars made by prepunion.c to varno *first_child_relid. + * Change the Vars made by prepunion.c that reference an RTE_SETOP entry + * to varno context->first_child_relid. + * + * This runs before the rtable is flattened, so Vars still carry their + * subquery-local varnos. */ static Node * -fix_dummy_setop_vars_mutator(Node *node, int *first_child_relid) +fix_dummy_setop_vars_mutator(Node *node, fix_dummy_setop_vars_context *context) { if (node == NULL) return NULL; @@ -2293,8 +2309,9 @@ fix_dummy_setop_vars_mutator(Node *node, int *first_child_relid) { Var *var = (Var *) node; - if (var->varno == 0) - return (Node *) makeVar(*first_child_relid, + if (var->varlevelsup == 0 && !IS_SPECIAL_VARNO(var->varno) && + planner_rt_fetch(var->varno, context->root)->rtekind == RTE_SETOP) + return (Node *) makeVar(context->first_child_relid, var->varattno, var->vartype, var->vartypmod, @@ -2303,7 +2320,7 @@ fix_dummy_setop_vars_mutator(Node *node, int *first_child_relid) return node; } return expression_tree_mutator(node, fix_dummy_setop_vars_mutator, - first_child_relid); + context); } /* diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 28921328abc..00b63fb80a8 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1668,6 +1668,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, case RTE_NAMEDTUPLESTORE: case RTE_RESULT: case RTE_GROUP: + case RTE_SETOP: /* these can't contain any lateral references */ break; } @@ -1811,13 +1812,19 @@ pull_up_simple_union_all(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte) { RangeTblEntry *child_rte = (RangeTblEntry *) lfirst(rt); + /* RTE_SETOP entries can't contain any lateral references */ + if (child_rte->rtekind == RTE_SETOP) + continue; Assert(child_rte->rtekind == RTE_SUBQUERY); child_rte->lateral = true; } } /* - * Append child RTEs (and their perminfos) to parent rtable. + * Append child RTEs (and their perminfos) to parent rtable. This also + * brings along the subquery's RTE_SETOP entries. Nothing in the parent + * will reference them, since the setOperations tree is not pulled up, so + * they are harmless dead entries. */ CombineRangeTables(&root->parse->rtable, &root->parse->rteperminfos, rtable, subquery->rteperminfos); @@ -2763,6 +2770,7 @@ replace_vars_in_jointree(Node *jtnode, case RTE_NAMEDTUPLESTORE: case RTE_RESULT: case RTE_GROUP: + case RTE_SETOP: /* these shouldn't be marked LATERAL */ Assert(false); break; diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index 02ac58be211..3d0a5e25303 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -74,6 +74,7 @@ static List *generate_setop_tlist(List *colTypes, List *colCollations, List *refnames_tlist, bool *trivial_tlist); static List *generate_append_tlist(List *colTypes, List *colCollations, + Index varno, List *input_tlists, List *refnames_tlist); static List *generate_setop_grouplist(SetOperationStmt *op, List *targetlist); @@ -299,7 +300,8 @@ recurse_set_operations(Node *setOp, PlannerInfo *root, * the subplan was generated by generate_union_paths() or * generate_nonunion_paths(), and hence its tlist was generated by * generate_append_tlist() or generate_setop_tlist(), this will work. - * We just tell generate_setop_tlist() to use varno 0. + * We just tell generate_setop_tlist() to use the RT index of this set + * operation's RTE_SETOP entry, as those functions did. */ if (!tlist_same_datatypes(*pTargetList, colTypes, false) || !tlist_same_collations(*pTargetList, colCollations, false)) @@ -309,7 +311,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root, ListCell *lc; *pTargetList = generate_setop_tlist(colTypes, colCollations, - 0, + op->rtindex, false, *pTargetList, refnames_tlist, @@ -418,6 +420,7 @@ generate_recursion_path(SetOperationStmt *setOp, PlannerInfo *root, * Generate tlist for RecursiveUnion path node --- same as in Append cases */ tlist = generate_append_tlist(setOp->colTypes, setOp->colCollations, + setOp->rtindex, list_make2(lpath_tlist, rpath_tlist), refnames_tlist); @@ -657,14 +660,12 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, * using the subroot->parse's original targetlist expressions, not the * subroot->processed_tlist which might seem more appropriate. The reason * is that if the subquery is itself a setop, it may return a - * processed_tlist containing "varno 0" Vars generated by - * generate_append_tlist, and those would confuse estimate_num_groups - * mightily. We ought to get rid of the "varno 0" hack, but that requires - * a redesign of the parsetree representation of setops, so that there can - * be an RTE corresponding to each setop's output. Note, we use this not - * subquery's targetlist but subroot->parse's targetlist, because it was - * revised by self-join removal. subquery's targetlist might contain the - * references to the removed relids. + * processed_tlist containing Vars that reference an RTE_SETOP entry, and + * estimate_num_groups can't cope with those since that RTE has no + * RelOptInfo. Note, we use this not subquery's targetlist but + * subroot->parse's targetlist, because it was revised by self-join + * removal. subquery's targetlist might contain the references to the + * removed relids. */ if (pNumGroups) { @@ -733,9 +734,13 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, * next plan level up. */ tlist = generate_append_tlist(op->colTypes, op->colCollations, + op->rtindex, 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) { @@ -866,9 +871,10 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, * Although we told create_append_path to assign NIL pathkeys to the * AppendPath, it may have overridden that (if there's just one surviving * child path, it will use that path's pathkeys). However, createplan.c - * will fail because the append relation's tlist contains "varno 0" Vars - * (cf. generate_append_tlist), which won't match what is in the pathkeys. - * We need to fix that someday, but for now, just force the AppendPath's + * will fail because the append relation's tlist contains Vars that + * reference the set operation's RTE_SETOP entry (cf. + * generate_append_tlist), which won't match what is in the pathkeys. We + * need to fix that someday, but for now, just force the AppendPath's * pathkeys back to NIL. */ apath->pathkeys = NIL; @@ -919,7 +925,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, create_append_path(root, result_rel, partial, NIL, NULL, parallel_workers, enable_parallel_append, -1); - /* See comment above about pathkeys vs. "varno 0" Vars */ + /* See comment above about pathkeys vs. RTE_SETOP Vars */ papath->pathkeys = NIL; gpath = (Path *) @@ -1110,7 +1116,7 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, * next plan level up. */ tlist = generate_setop_tlist(op->colTypes, op->colCollations, - 0, false, lpath_tlist, refnames_tlist, + op->rtindex, false, lpath_tlist, refnames_tlist, &result_trivial_tlist); /* We should not have needed any type coercions in the tlist */ @@ -1118,6 +1124,9 @@ 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); @@ -1139,6 +1148,14 @@ 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); } /* @@ -1238,7 +1255,7 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, apath = (Path *) create_append_path(root, result_rel, append, NIL, NULL, 0, false, -1); - /* See comment in generate_union_paths about "varno 0" Vars */ + /* See comment in generate_union_paths about RTE_SETOP Vars */ apath->pathkeys = NIL; add_path(result_rel, apath); @@ -1610,19 +1627,22 @@ generate_setop_tlist(List *colTypes, List *colCollations, * * colTypes: OID list of set-op's result column datatypes * colCollations: OID list of set-op's result column collations + * varno: varno to use in generated Vars * input_tlists: list of tlists for sub-plans of the Append * refnames_tlist: targetlist to take column names from * * The entries in the Append's targetlist should always be simple Vars; * we just have to make sure they have the right datatypes/typmods/collations. - * The Vars are always generated with varno 0. + * The Vars reference the set operation's RTE_SETOP entry. * - * XXX a problem with the varno-zero approach is that set_pathtarget_cost_width - * cannot figure out a realistic width for the tlist we make here. But we - * ought to refactor this code to produce a PathTarget directly, anyway. + * XXX a problem with this approach is that set_pathtarget_cost_width cannot + * figure out a realistic width for the tlist we make here, since there is no + * RelOptInfo for an RTE_SETOP entry. But we ought to refactor this code to + * produce a PathTarget directly, anyway. */ static List * generate_append_tlist(List *colTypes, List *colCollations, + Index varno, List *input_tlists, List *refnames_tlist) { @@ -1693,7 +1713,7 @@ generate_append_tlist(List *colTypes, List *colCollations, Assert(reftle->resno == resno); Assert(!reftle->resjunk); - expr = (Node *) makeVar(0, + expr = (Node *) makeVar(varno, resno, colType, colTypmod, @@ -1766,9 +1786,10 @@ generate_setop_grouplist(SetOperationStmt *op, List *targetlist) * PathTarget's width to the average width of the Paths in child_pathlist * weighted using the estimated row count of each path. * - * Note: This is required because set op target lists use varno==0, which - * results in a type default width estimate rather than one that's based on - * statistics of the columns from the set op children. + * Note: This is required because set op target lists contain Vars that + * reference an RTE_SETOP entry, which has no RelOptInfo. That results in a + * type default width estimate rather than one that's based on statistics of + * the columns from the set op children. */ static PathTarget * create_setop_pathtarget(PlannerInfo *root, List *tlist, List *child_pathlist) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 2dc81a8d3b2..c84a5e36180 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -79,6 +79,7 @@ static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt); static Query *transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt); static Node *transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, bool isTopLevel, List **targetlist); +static List *addSetOpRTEs(ParseState *pstate, Node *node); static void constructSetOpTargetlist(ParseState *pstate, SetOperationStmt *op, const List *ltargetlist, const List *rtargetlist, List **targetlist, const char *context, bool recursive); @@ -1882,6 +1883,9 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) Assert(sostmt); qry->setOperations = (Node *) sostmt; + /* Add an RTE_SETOP entry for each set operation in the tree */ + (void) addSetOpRTEs(pstate, (Node *) sostmt); + /* * Re-find leftmost SELECT (now it's a sub-query in rangetable) */ @@ -2079,6 +2083,50 @@ makeSortGroupClauseForSetOp(Oid rescoltype, bool require_hash) return grpcl; } +/* + * addSetOpRTEs + * Add an RTE_SETOP range table entry for each node of a set-op tree + * + * This is done after transformSetOperationTree has added RTEs for all the + * leaf subqueries, so the RTE_SETOP entries come after all the leaves, in + * postorder. Each node's rtindex is set to the index of its entry. + * + * Returns the output column names of the given node, which are those of its + * leftmost leaf. + */ +static List * +addSetOpRTEs(ParseState *pstate, Node *node) +{ + /* Guard against stack overflow due to overly complex setop nests */ + check_stack_depth(); + + if (IsA(node, RangeTblRef)) + { + RangeTblRef *rtr = (RangeTblRef *) node; + + return rt_fetch(rtr->rtindex, pstate->p_rtable)->eref->colnames; + } + else if (IsA(node, SetOperationStmt)) + { + SetOperationStmt *op = (SetOperationStmt *) node; + List *colnames; + + colnames = addSetOpRTEs(pstate, op->larg); + (void) addSetOpRTEs(pstate, op->rarg); + + op->rtindex = addRangeTableEntryForSetOp(pstate, + colnames, + list_copy(op->colTypes), + list_copy(op->colTypmods), + list_copy(op->colCollations)); + + return colnames; + } + + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); + return NIL; /* keep compiler quiet */ +} + /* * transformSetOperationTree * Recursively transform leaves and internal nodes of a set-op tree diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 8aeb3e219ac..394f190ba85 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -2668,6 +2668,51 @@ addRangeTableEntryForGroup(ParseState *pstate, return nsitem; } +/* + * Add an entry for the output of a set operation to the pstate's range table + * (p_rtable), and return its RT index. + * + * Nothing can refer to a set operation's output by name, so unlike the other + * functions here we don't build a ParseNamespaceItem. + */ +Index +addRangeTableEntryForSetOp(ParseState *pstate, + List *colnames, + List *coltypes, + List *coltypmods, + List *colcollations) +{ + RangeTblEntry *rte = makeNode(RangeTblEntry); + + Assert(pstate != NULL); + Assert(list_length(colnames) == list_length(coltypes)); + + rte->rtekind = RTE_SETOP; + rte->alias = NULL; + rte->eref = makeAlias("*SETOP*", copyObject(colnames)); + rte->coltypes = coltypes; + rte->coltypmods = coltypmods; + rte->colcollations = colcollations; + + /* + * Set flags. + * + * A set operation's output is never checked for access rights, so no need + * to perform addRTEPermissionInfo(). + */ + rte->lateral = false; + rte->inFromCl = false; + + /* + * Add completed RTE to pstate's range table list, so that we know its + * index. But we don't add it to the join list --- caller must do that if + * appropriate. + */ + pstate->p_rtable = lappend(pstate->p_rtable, rte); + + return list_length(pstate->p_rtable); +} + /* * Has the specified refname been selected FOR UPDATE/FOR SHARE? @@ -3059,8 +3104,9 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, case RTE_VALUES: case RTE_CTE: case RTE_NAMEDTUPLESTORE: + case RTE_SETOP: { - /* Tablefunc, Values, CTE, or ENR RTE */ + /* Tablefunc, Values, CTE, ENR, or SetOp RTE */ ListCell *aliasp_item = list_head(rte->eref->colnames); ListCell *lct; ListCell *lcm; @@ -3443,10 +3489,11 @@ get_rte_attribute_is_dropped(RangeTblEntry *rte, AttrNumber attnum) case RTE_VALUES: case RTE_CTE: case RTE_GROUP: + case RTE_SETOP: /* - * Subselect, Table Functions, Values, CTE, GROUP RTEs never have - * dropped columns + * Subselect, Table Functions, Values, CTE, GROUP, SetOp RTEs + * never have dropped columns */ result = false; break; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 728936b502e..d171e545057 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -422,6 +422,9 @@ markTargetListOrigin(ParseState *pstate, TargetEntry *tle, case RTE_GROUP: /* We couldn't get here: the RTE_GROUP RTE has not been added */ break; + case RTE_SETOP: + /* We couldn't get here: no Var in a parse tree references it */ + break; } } @@ -1691,6 +1694,13 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup) * We couldn't get here: the RTE_GROUP RTE has not been added. */ break; + case RTE_SETOP: + + /* + * We couldn't get here: no Var in a parse tree references an + * RTE_SETOP RTE. + */ + break; } /* diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c index 7011275f8c6..cbfe8bf1d93 100644 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -372,7 +372,8 @@ CombineRangeTables(List **dst_rtable, List **dst_perminfos, * Find all Var nodes in the given tree with varlevelsup == sublevels_up, * and increment their varno fields (rangetable indexes) by 'offset'. * The varnosyn fields are adjusted similarly. Also, adjust other nodes - * that contain rangetable indexes, such as RangeTblRef and JoinExpr. + * that contain rangetable indexes, such as RangeTblRef, JoinExpr and + * SetOperationStmt. * * NOTE: although this has the form of a walker, we cheat and modify the * nodes in-place. The given expression tree should have been copied @@ -429,6 +430,14 @@ OffsetVarNodes_walker(Node *node, OffsetVarNodes_context *context) j->rtindex += context->offset; /* fall through to examine children */ } + if (IsA(node, SetOperationStmt)) + { + SetOperationStmt *op = (SetOperationStmt *) node; + + if (op->rtindex && context->sublevels_up == 0) + op->rtindex += context->offset; + /* fall through to examine children */ + } if (IsA(node, PlaceHolderVar)) { PlaceHolderVar *phv = (PlaceHolderVar *) node; @@ -531,7 +540,7 @@ OffsetVarNodes(Node *node, int offset, int sublevels_up) * (identified by sublevels_up and rt_index), and change their varno fields * to 'new_index', and update varnosyn and varnullingrels fields similarly. * Also adjust other nodes that contain rangetable indexes, such as - * RangeTblRef and JoinExpr. + * RangeTblRef, JoinExpr and SetOperationStmt. * * Also, new_index can be INVALID_VAR to indicate that we are deleting the * given relid from the tree. In this case we expect to find rt_index only @@ -615,6 +624,18 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context) } /* fall through to examine children */ } + if (IsA(node, SetOperationStmt)) + { + SetOperationStmt *op = (SetOperationStmt *) node; + + if (context->sublevels_up == 0 && + op->rtindex == context->rt_index) + { + Assert(context->new_index != INVALID_VAR); + op->rtindex = context->new_index; + } + /* fall through to examine children */ + } if (IsA(node, PlaceHolderVar)) { PlaceHolderVar *phv = (PlaceHolderVar *) node; @@ -1037,6 +1058,15 @@ rangeTableEntry_used_walker(Node *node, return true; /* fall through to examine children */ } + if (IsA(node, SetOperationStmt)) + { + SetOperationStmt *op = (SetOperationStmt *) node; + + if (op->rtindex == context->rt_index && + context->sublevels_up == 0) + return true; + /* fall through to examine children */ + } /* Shouldn't need to handle planner auxiliary nodes here */ Assert(!IsA(node, PlaceHolderVar)); Assert(!IsA(node, PlanRowMark)); diff --git a/src/backend/rewrite/rewriteSearchCycle.c b/src/backend/rewrite/rewriteSearchCycle.c index 75943072817..131c46b8d08 100644 --- a/src/backend/rewrite/rewriteSearchCycle.c +++ b/src/backend/rewrite/rewriteSearchCycle.c @@ -208,7 +208,8 @@ rewriteSearchAndCycle(CommonTableExpr *cte) rti2; RangeTblEntry *rte1, *rte2, - *newrte; + *newrte, + *setoprte; Query *newq1, *newq2; Query *newsubquery; @@ -626,6 +627,26 @@ rewriteSearchAndCycle(CommonTableExpr *cte) makeSortGroupClauseForSetOp(RECORDARRAYOID, true)); } + /* + * Likewise for the RTE_SETOP entry representing the SetOperationStmt's + * output + */ + setoprte = rt_fetch(sos->rtindex, ctequery->rtable); + Assert(setoprte->rtekind == RTE_SETOP); + if (cte->search_clause) + setoprte->eref->colnames = lappend(setoprte->eref->colnames, + makeString(cte->search_clause->search_seq_column)); + if (cte->cycle_clause) + { + setoprte->eref->colnames = lappend(setoprte->eref->colnames, + makeString(cte->cycle_clause->cycle_mark_column)); + setoprte->eref->colnames = lappend(setoprte->eref->colnames, + makeString(cte->cycle_clause->cycle_path_column)); + } + setoprte->coltypes = list_copy(sos->colTypes); + setoprte->coltypmods = list_copy(sos->colTypmods); + setoprte->colcollations = list_copy(sos->colCollations); + /* * Add the additional columns to the CTE query's target list */ diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 8f6a46fa614..4d9d171ae35 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8468,6 +8468,14 @@ get_name_for_var_field(Var *var, int fieldno, * expressions. */ break; + case RTE_SETOP: + + /* + * We couldn't get here: Vars that reference an RTE_SETOP RTE + * appear only in plan trees, where they have been replaced by + * references to the child plan's output. + */ + break; } /* diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index 5ee377595bd..291036d91c1 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -2267,15 +2267,18 @@ estimate_array_length(PlannerInfo *root, Node *arrayexpr) double nelem = 0; /* - * Skip calling examine_variable for Var with varno 0, which has no - * valid relation entry and would error in find_base_rel. Such a Var - * can appear when a nested set operation's output type doesn't match - * the parent's expected type, because recurse_set_operations builds a - * projection target list using generate_setop_tlist with varno 0, and - * if the required type coercion involves an ArrayCoerceExpr, we can - * be called on that Var. + * Skip calling examine_variable for a Var referencing an RTE_SETOP + * entry, which has no RelOptInfo and would error in find_base_rel. + * Such a Var can appear when a nested set operation's output type + * doesn't match the parent's expected type, because + * recurse_set_operations builds a projection target list using + * generate_setop_tlist, and if the required type coercion involves an + * ArrayCoerceExpr, we can be called on that Var. */ - if (IsA(arrayexpr, Var) && ((Var *) arrayexpr)->varno == 0) + if (IsA(arrayexpr, Var) && + ((Var *) arrayexpr)->varlevelsup == 0 && + !IS_SPECIAL_VARNO(((Var *) arrayexpr)->varno) && + planner_rt_fetch(((Var *) arrayexpr)->varno, root)->rtekind == RTE_SETOP) return 10; /* default guess, should match scalararraysel */ examine_variable(root, arrayexpr, 0, &vardata); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0debcd193ab..f28d5b355dc 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1055,6 +1055,7 @@ typedef enum RTEKind * RTEs are added by the planner, they're not * present during parsing or rewriting */ RTE_GROUP, /* the grouping step */ + RTE_SETOP, /* output of a set operation */ } RTEKind; typedef struct RangeTblEntry @@ -1234,7 +1235,8 @@ typedef struct RangeTblEntry bool self_reference pg_node_attr(query_jumble_ignore); /* - * Fields valid for CTE, VALUES, ENR, and TableFunc RTEs (else NIL): + * Fields valid for CTE, VALUES, ENR, TableFunc, and SetOp RTEs (else + * NIL): * * We need these for CTE RTEs so that the types of self-referential * columns are well-defined. For VALUES RTEs, storing these explicitly @@ -1242,9 +1244,9 @@ typedef struct RangeTblEntry * ENRs, we store the types explicitly here (we could get the information * from the catalogs if 'relid' was supplied, but we'd still need these * for TupleDesc-based ENRs, so we might as well always store the type - * info here). For TableFuncs, these fields are redundant with data in - * the TableFunc node, but keeping them here allows some code sharing with - * the other cases. + * info here). For TableFuncs and SetOps, these fields are redundant with + * data in the TableFunc or SetOperationStmt node, but keeping them here + * allows some code sharing with the other cases. * * For ENRs only, we have to consider the possibility of dropped columns. * A dropped column is included in these lists, but it will have zeroes in @@ -2300,7 +2302,11 @@ typedef struct SetOperationStmt Node *rarg; /* right child */ /* Eventually add fields for CORRESPONDING spec here */ - /* Fields derived during parse analysis: */ + /* + * Fields derived during parse analysis. The RTE_SETOP entry at rtindex + * holds copies of colTypes, colTypmods and colCollations, so anything + * that changes these must update that entry too. + */ /* OID list of output column type OIDs */ List *colTypes pg_node_attr(query_jumble_ignore); /* integer list of output column typmods */ @@ -2310,6 +2316,8 @@ typedef struct SetOperationStmt /* a list of SortGroupClause's */ List *groupClauses pg_node_attr(query_jumble_ignore); /* groupClauses is NIL if UNION ALL, but must be set otherwise */ + /* RT index of the RTE_SETOP entry representing this node's output */ + int rtindex pg_node_attr(query_jumble_ignore); } SetOperationStmt; diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 64f87da1674..eac439578c6 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -191,6 +191,7 @@ 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/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index ecfb4c047c0..583cc06ebd8 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -104,6 +104,11 @@ extern ParseNamespaceItem *addRangeTableEntryForENR(ParseState *pstate, bool inFromCl); extern ParseNamespaceItem *addRangeTableEntryForGroup(ParseState *pstate, List *groupClauses); +extern Index addRangeTableEntryForSetOp(ParseState *pstate, + List *colnames, + List *coltypes, + List *coltypmods, + List *colcollations); extern RTEPermissionInfo *addRTEPermissionInfo(List **rteperminfos, RangeTblEntry *rte); extern RTEPermissionInfo *getRTEPermissionInfo(List *rteperminfos, diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out index 1e6e020fea8..6a4246748df 100644 --- a/src/test/regress/expected/incremental_sort.out +++ b/src/test/regress/expected/incremental_sort.out @@ -1468,7 +1468,7 @@ explain (costs off) select a,b,sum(c) from t group by 1,2 order by 1,2,3 limit 1 -> Parallel Index Scan using t_a_idx on t (14 rows) --- Incremental sort vs. set operations with varno 0 +-- Incremental sort vs. set operations with RTE_SETOP Vars set enable_hashagg to off; explain (costs off) select * from t union select * from t order by 1,3; QUERY PLAN diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index 2495351d907..9cffe854b19 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -1486,6 +1486,119 @@ EXCEPT One-Time Filter: false (8 rows) +-- Also check a dummy set operation that has no child plan at all. Here the +-- Result's targetlist holds the set operation's output Vars directly; the +-- coercion is done in the leaf. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +UNION ALL +SELECT ten::int8 FROM tenk1 WHERE 1=2; + QUERY PLAN +--------------------------------------------------------------- + Result + Output: unnamed_subquery.two + Replaces: Aggregate on unnamed_subquery, unnamed_subquery_1 + One-Time Filter: false +(4 rows) + +-- Ensure a nested set operation whose output is coerced by its parent +-- refers to the right child output +EXPLAIN (COSTS OFF, VERBOSE) +(SELECT two FROM tenk1 t1 + UNION + SELECT four FROM tenk1 t2) +INTERSECT +SELECT ten::int8 FROM tenk1 t3 +ORDER BY 1; + QUERY PLAN +----------------------------------------------------- + Sort + Output: ((t1.two)::bigint) + Sort Key: ((t1.two)::bigint) + -> HashSetOp Intersect + Output: ((t1.two)::bigint) + -> HashAggregate + Output: t1.two + Group Key: t1.two + -> Append + -> Seq Scan on public.tenk1 t1 + Output: t1.two + -> Seq Scan on public.tenk1 t2 + Output: t2.four + -> Seq Scan on public.tenk1 t3 + Output: (t3.ten)::bigint +(15 rows) + +(SELECT two FROM tenk1 t1 + UNION + SELECT four FROM tenk1 t2) +INTERSECT +SELECT ten::int8 FROM tenk1 t3 +ORDER BY 1; + two +----- + 0 + 1 + 2 + 3 +(4 rows) + +-- Ensure the sort order of a nested set operation is recognized by its +-- parent +SET enable_hashagg TO off; +EXPLAIN (COSTS OFF) +SELECT unique1 FROM tenk1 INTERSECT SELECT unique2 FROM tenk1 +UNION +SELECT unique1 FROM tenk1 WHERE unique1 < 5; + QUERY PLAN +------------------------------------------------------------------------ + Unique + -> Merge Append + Sort Key: tenk1.unique1 + -> SetOp Intersect + -> Index Only Scan using tenk1_unique1 on tenk1 + -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1 + -> Index Only Scan using tenk1_unique1 on tenk1 tenk1_2 + Index Cond: (unique1 < 5) +(8 rows) + +EXPLAIN (COSTS OFF) +(SELECT unique1 FROM tenk1 UNION SELECT unique2 FROM tenk1) +EXCEPT +(SELECT unique1 FROM tenk1 INTERSECT SELECT unique2 FROM tenk1 WHERE unique2 > 9990) +ORDER BY 1; + QUERY PLAN +------------------------------------------------------------------------ + SetOp Except + -> Unique + -> Merge Append + Sort Key: tenk1.unique1 + -> Index Only Scan using tenk1_unique1 on tenk1 + -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1 + -> SetOp Intersect + -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_2 + Index Cond: (unique2 > 9990) + -> Index Only Scan using tenk1_unique1 on tenk1 tenk1_3 +(10 rows) + +EXPLAIN (COSTS OFF) +(SELECT unique1 FROM tenk1 UNION ALL SELECT unique2 FROM tenk1) +EXCEPT +SELECT unique1 FROM tenk1 WHERE unique1 < 5 +ORDER BY 1; + QUERY PLAN +------------------------------------------------------------------------ + SetOp Except + -> Sort + Sort Key: tenk1.unique1 + -> Append + -> Index Only Scan using tenk1_unique1 on tenk1 + -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1 + -> Index Only Scan using tenk1_unique1 on tenk1 tenk1_2 + Index Cond: (unique1 < 5) +(8 rows) + +RESET enable_hashagg; -- Test constraint exclusion of UNION ALL subqueries explain (costs off) SELECT * FROM @@ -1770,7 +1883,7 @@ on true limit 1; -> Result (6 rows) --- Test handling of Vars with varno 0 in estimate_array_length +-- Test handling of RTE_SETOP Vars in estimate_array_length explain (verbose, costs off) select null::int[] union all select null::int[] union all select null::bigint[]; QUERY PLAN diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sql index bbe658a7588..16340fa37a7 100644 --- a/src/test/regress/sql/incremental_sort.sql +++ b/src/test/regress/sql/incremental_sort.sql @@ -218,7 +218,7 @@ explain (costs off) select a,b,sum(c) from t group by 1,2 order by 1,2,3 limit 1 set enable_incremental_sort = on; explain (costs off) select a,b,sum(c) from t group by 1,2 order by 1,2,3 limit 1; --- Incremental sort vs. set operations with varno 0 +-- Incremental sort vs. set operations with RTE_SETOP Vars set enable_hashagg to off; explain (costs off) select * from t union select * from t order by 1,3; diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql index 8fce4528a3f..59e376b73a4 100644 --- a/src/test/regress/sql/union.sql +++ b/src/test/regress/sql/union.sql @@ -567,6 +567,52 @@ EXCEPT EXCEPT ALL SELECT ten FROM tenk1 t2); +-- Also check a dummy set operation that has no child plan at all. Here the +-- Result's targetlist holds the set operation's output Vars directly; the +-- coercion is done in the leaf. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +UNION ALL +SELECT ten::int8 FROM tenk1 WHERE 1=2; + +-- Ensure a nested set operation whose output is coerced by its parent +-- refers to the right child output +EXPLAIN (COSTS OFF, VERBOSE) +(SELECT two FROM tenk1 t1 + UNION + SELECT four FROM tenk1 t2) +INTERSECT +SELECT ten::int8 FROM tenk1 t3 +ORDER BY 1; + +(SELECT two FROM tenk1 t1 + UNION + SELECT four FROM tenk1 t2) +INTERSECT +SELECT ten::int8 FROM tenk1 t3 +ORDER BY 1; + +-- Ensure the sort order of a nested set operation is recognized by its +-- parent +SET enable_hashagg TO off; +EXPLAIN (COSTS OFF) +SELECT unique1 FROM tenk1 INTERSECT SELECT unique2 FROM tenk1 +UNION +SELECT unique1 FROM tenk1 WHERE unique1 < 5; + +EXPLAIN (COSTS OFF) +(SELECT unique1 FROM tenk1 UNION SELECT unique2 FROM tenk1) +EXCEPT +(SELECT unique1 FROM tenk1 INTERSECT SELECT unique2 FROM tenk1 WHERE unique2 > 9990) +ORDER BY 1; + +EXPLAIN (COSTS OFF) +(SELECT unique1 FROM tenk1 UNION ALL SELECT unique2 FROM tenk1) +EXCEPT +SELECT unique1 FROM tenk1 WHERE unique1 < 5 +ORDER BY 1; +RESET enable_hashagg; + -- Test constraint exclusion of UNION ALL subqueries explain (costs off) SELECT * FROM @@ -701,7 +747,7 @@ left join lateral (select t1.tenthous from tenk2 t2 union all (values(1))) on true limit 1; --- Test handling of Vars with varno 0 in estimate_array_length +-- Test handling of RTE_SETOP Vars in estimate_array_length explain (verbose, costs off) select null::int[] union all select null::int[] union all select null::bigint[]; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b964e5ee907..119ec8e8e23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3780,6 +3780,7 @@ finalize_primnode_context find_dependent_phvs_context find_expr_references_context fireRIRonSubLink_context +fix_dummy_setop_vars_context fix_join_expr_context fix_scan_expr_context fix_upper_expr_context -- 2.37.1 (Apple Git-137.1)