aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2024-12-19 17:02:25 -0500
committerTom Lane <tgl@sss.pgh.pa.us>2024-12-19 17:02:25 -0500
commit8d96f57d5cc79c0c51050bb707c19bf07d2895eb (patch)
tree6d67ac0e73bee8d2d7fc9c977f3539fbc0430a46
parent27627929528e24a547d1058a5444b35491057a56 (diff)
downloadpostgresql-8d96f57d5cc79c0c51050bb707c19bf07d2895eb.tar.gz
postgresql-8d96f57d5cc79c0c51050bb707c19bf07d2895eb.zip
Improve planner's handling of SetOp plans.
Remove the code for inserting flag columns in the inputs of a SetOp. That was the only reason why there would be resjunk columns in a set-operations plan tree, so we can get rid of some code that supported that, too. Get rid of choose_hashed_setop() in favor of building Paths for the hashed and sorted alternatives, and letting them fight it out within add_path(). Remove set_operation_ordered_results_useful(), which was giving wrong answers due to examining the wrong ancestor node: we need to examine the immediate SetOperationStmt parent not the topmost node. Instead make each caller of recurse_set_operations() pass down the relevant parent node. (This thinko seems to have led only to wasted planning cycles and possibly-inferior plans, not wrong query answers. Perhaps we should back-patch it, but I'm not doing so right now.) Teach generate_nonunion_paths() to consider pre-sorted inputs for sorted SetOps, rather than always generating a Sort node. Patch by me; thanks to Richard Guo and David Rowley for review. Discussion: https://postgr.es/m/1850138.1731549611@sss.pgh.pa.us
-rw-r--r--src/backend/optimizer/plan/planner.c7
-rw-r--r--src/backend/optimizer/prep/prepunion.c478
-rw-r--r--src/backend/optimizer/util/pathnode.c69
-rw-r--r--src/include/optimizer/prep.h1
-rw-r--r--src/test/regress/expected/subselect.out48
-rw-r--r--src/test/regress/expected/union.out53
-rw-r--r--src/test/regress/sql/subselect.sql32
-rw-r--r--src/test/regress/sql/union.sql9
8 files changed, 365 insertions, 332 deletions
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index f3856c519f6..7468961b017 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -616,7 +616,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
* setops is used for set operation subqueries to provide the subquery with
* the context in which it's being used so that Paths correctly sorted for the
* set operation can be generated. NULL when not planning a set operation
- * child.
+ * child, or when a child of a set op that isn't interested in sorted input.
*
* Basically, this routine does the stuff that should only be done once
* per Query object. It then calls grouping_planner. At one time,
@@ -1350,7 +1350,7 @@ preprocess_phv_expression(PlannerInfo *root, Expr *expr)
* setops is used for set operation subqueries to provide the subquery with
* the context in which it's being used so that Paths correctly sorted for the
* set operation can be generated. NULL when not planning a set operation
- * child.
+ * child, or when a child of a set op that isn't interested in sorted input.
*
* Returns nothing; the useful output is in the Paths we attach to the
* (UPPERREL_FINAL, NULL) upperrel in *root. In addition,
@@ -3467,8 +3467,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
tlist);
/* setting setop_pathkeys might be useful to the union planner */
- if (qp_extra->setop != NULL &&
- set_operation_ordered_results_useful(qp_extra->setop))
+ if (qp_extra->setop != NULL)
{
List *groupClauses;
bool sortable;
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 1c43370005e..698ef601be1 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -39,9 +39,9 @@
static RelOptInfo *recurse_set_operations(Node *setOp, PlannerInfo *root,
+ SetOperationStmt *parentOp,
List *colTypes, List *colCollations,
- bool junkOK,
- int flag, List *refnames_tlist,
+ List *refnames_tlist,
List **pTargetList,
bool *istrivial_tlist);
static RelOptInfo *generate_recursion_path(SetOperationStmt *setOp,
@@ -64,19 +64,13 @@ static List *plan_union_children(PlannerInfo *root,
List **tlist_list,
List **istrivial_tlist);
static void postprocess_setop_rel(PlannerInfo *root, RelOptInfo *rel);
-static bool choose_hashed_setop(PlannerInfo *root, List *groupClauses,
- Path *lpath, Path *rpath,
- double dNumGroups, double dNumOutputRows,
- const char *construct);
static List *generate_setop_tlist(List *colTypes, List *colCollations,
- int flag,
Index varno,
bool hack_constants,
List *input_tlist,
List *refnames_tlist,
bool *trivial_tlist);
static List *generate_append_tlist(List *colTypes, List *colCollations,
- bool flag,
List *input_tlists,
List *refnames_tlist);
static List *generate_setop_grouplist(SetOperationStmt *op, List *targetlist);
@@ -160,12 +154,11 @@ plan_set_operations(PlannerInfo *root)
/*
* Recurse on setOperations tree to generate paths for set ops. The
* final output paths should have just the column types shown as the
- * output from the top-level node, plus possibly resjunk working
- * columns (we can rely on upper-level nodes to deal with that).
+ * output from the top-level node.
*/
setop_rel = recurse_set_operations((Node *) topop, root,
+ NULL, /* no parent */
topop->colTypes, topop->colCollations,
- true, -1,
leftmostQuery->targetList,
&top_tlist,
&trivial_tlist);
@@ -178,49 +171,35 @@ plan_set_operations(PlannerInfo *root)
}
/*
- * set_operation_ordered_results_useful
- * Return true if the given SetOperationStmt can be executed by utilizing
- * paths that provide sorted input according to the setop's targetlist.
- * Returns false when sorted paths are not any more useful than unsorted
- * ones.
- */
-bool
-set_operation_ordered_results_useful(SetOperationStmt *setop)
-{
- /*
- * Paths sorted by the targetlist are useful for UNION as we can opt to
- * MergeAppend the sorted paths then Unique them. Ordered paths are no
- * more useful than unordered ones for UNION ALL.
- */
- if (!setop->all && setop->op == SETOP_UNION)
- return true;
-
- /*
- * EXCEPT / EXCEPT ALL / INTERSECT / INTERSECT ALL cannot yet utilize
- * correctly sorted input paths.
- */
- return false;
-}
-
-/*
* recurse_set_operations
* Recursively handle one step in a tree of set operations
*
+ * setOp: current step (could be a SetOperationStmt or a leaf RangeTblRef)
+ * parentOp: parent step, or NULL if none (but see below)
* colTypes: OID list of set-op's result column datatypes
* colCollations: OID list of set-op's result column collations
- * junkOK: if true, child resjunk columns may be left in the result
- * flag: if >= 0, add a resjunk output column indicating value of flag
* refnames_tlist: targetlist to take column names from
*
+ * parentOp should be passed as NULL unless that step is interested in
+ * getting sorted output from this step. ("Sorted" means "sorted according
+ * to the default btree opclasses of the result column datatypes".)
+ *
* Returns a RelOptInfo for the subtree, as well as these output parameters:
* *pTargetList: receives the fully-fledged tlist for the subtree's top plan
* *istrivial_tlist: true if, and only if, datatypes between parent and child
* match.
*
+ * If setOp is a leaf node, this function plans the sub-query but does
+ * not populate the pathlist of the returned RelOptInfo. The caller will
+ * generate SubqueryScan paths using useful path(s) of the subquery (see
+ * build_setop_child_paths). But this function does build the paths for
+ * set-operation nodes.
+ *
* The pTargetList output parameter is mostly redundant with the pathtarget
* of the returned RelOptInfo, but for the moment we need it because much of
* the logic in this file depends on flag columns being marked resjunk.
- * Pending a redesign of how that works, this is the easy way out.
+ * XXX Now that there are no flag columns and hence no resjunk columns, we
+ * could probably refactor this file to deal only in pathtargets.
*
* We don't have to care about typmods here: the only allowed difference
* between set-op input and output typmods is input is a specific typmod
@@ -228,9 +207,9 @@ set_operation_ordered_results_useful(SetOperationStmt *setop)
*/
static RelOptInfo *
recurse_set_operations(Node *setOp, PlannerInfo *root,
+ SetOperationStmt *parentOp,
List *colTypes, List *colCollations,
- bool junkOK,
- int flag, List *refnames_tlist,
+ List *refnames_tlist,
List **pTargetList,
bool *istrivial_tlist)
{
@@ -245,7 +224,6 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
{
RangeTblRef *rtr = (RangeTblRef *) setOp;
RangeTblEntry *rte = root->simple_rte_array[rtr->rtindex];
- SetOperationStmt *setops;
Query *subquery = rte->subquery;
PlannerInfo *subroot;
List *tlist;
@@ -260,15 +238,13 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
Assert(root->plan_params == NIL);
/*
- * Pass the set operation details to the subquery_planner to have it
- * consider generating Paths correctly ordered for the set operation.
+ * Generate a subroot and Paths for the subquery. If we have a
+ * parentOp, pass that down to encourage subquery_planner to consider
+ * suitably-sorted Paths.
*/
- setops = castNode(SetOperationStmt, root->parse->setOperations);
-
- /* Generate a subroot and Paths for the subquery */
subroot = rel->subroot = subquery_planner(root->glob, subquery, root,
false, root->tuple_fraction,
- setops);
+ parentOp);
/*
* It should not be possible for the primitive query to contain any
@@ -279,7 +255,6 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
/* Figure out the appropriate target list for this subquery. */
tlist = generate_setop_tlist(colTypes, colCollations,
- flag,
rtr->rtindex,
true,
subroot->processed_tlist,
@@ -318,16 +293,14 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
* generate_append_tlist() or generate_setop_tlist(), this will work.
* We just tell generate_setop_tlist() to use varno 0.
*/
- if (flag >= 0 ||
- !tlist_same_datatypes(*pTargetList, colTypes, junkOK) ||
- !tlist_same_collations(*pTargetList, colCollations, junkOK))
+ if (!tlist_same_datatypes(*pTargetList, colTypes, false) ||
+ !tlist_same_collations(*pTargetList, colCollations, false))
{
PathTarget *target;
bool trivial_tlist;
ListCell *lc;
*pTargetList = generate_setop_tlist(colTypes, colCollations,
- flag,
0,
false,
*pTargetList,
@@ -410,8 +383,8 @@ generate_recursion_path(SetOperationStmt *setOp, PlannerInfo *root,
* separately without any intention of combining them into one Append.
*/
lrel = recurse_set_operations(setOp->larg, root,
+ NULL, /* no value in sorted results */
setOp->colTypes, setOp->colCollations,
- false, -1,
refnames_tlist,
&lpath_tlist,
&lpath_trivial_tlist);
@@ -422,8 +395,8 @@ generate_recursion_path(SetOperationStmt *setOp, PlannerInfo *root,
/* The right path will want to look at the left one ... */
root->non_recursive_path = lpath;
rrel = recurse_set_operations(setOp->rarg, root,
+ NULL, /* no value in sorted results */
setOp->colTypes, setOp->colCollations,
- false, -1,
refnames_tlist,
&rpath_tlist,
&rpath_trivial_tlist);
@@ -436,7 +409,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, false,
+ tlist = generate_append_tlist(setOp->colTypes, setOp->colCollations,
list_make2(lpath_tlist, rpath_tlist),
refnames_tlist);
@@ -495,6 +468,10 @@ generate_recursion_path(SetOperationStmt *setOp, PlannerInfo *root,
* build_setop_child_paths
* Build paths for the set op child relation denoted by 'rel'.
*
+ * 'rel' is an RTE_SUBQUERY relation. We have already generated paths within
+ * the subquery's subroot; the task here is to create SubqueryScan paths for
+ * 'rel', representing scans of the useful subquery paths.
+ *
* interesting_pathkeys: if not NIL, also include paths that suit these
* pathkeys, sorting any unsorted paths as required.
* *pNumGroups: if not NULL, we estimate the number of distinct groups
@@ -736,7 +713,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root,
* concerned, but we must make it look real anyway for the benefit of the
* next plan level up.
*/
- tlist = generate_append_tlist(op->colTypes, op->colCollations, false,
+ tlist = generate_append_tlist(op->colTypes, op->colCollations,
tlist_list, refnames_tlist);
*pTargetList = tlist;
@@ -1033,11 +1010,13 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
bool lpath_trivial_tlist,
rpath_trivial_tlist,
result_trivial_tlist;
+ List *nonunion_pathkeys = NIL;
double dLeftGroups,
dRightGroups,
dNumGroups,
dNumOutputRows;
- bool use_hash;
+ bool can_sort;
+ bool can_hash;
SetOpCmd cmd;
/*
@@ -1047,26 +1026,69 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
/* Recurse on children */
lrel = recurse_set_operations(op->larg, root,
+ op,
op->colTypes, op->colCollations,
- false, -1,
refnames_tlist,
&lpath_tlist,
&lpath_trivial_tlist);
- if (lrel->rtekind == RTE_SUBQUERY)
- build_setop_child_paths(root, lrel, lpath_trivial_tlist, lpath_tlist,
- NIL, &dLeftGroups);
- else
- dLeftGroups = lrel->rows;
rrel = recurse_set_operations(op->rarg, root,
+ op,
op->colTypes, op->colCollations,
- false, -1,
refnames_tlist,
&rpath_tlist,
&rpath_trivial_tlist);
+
+ /*
+ * Generate tlist for SetOp plan node.
+ *
+ * The tlist for a SetOp plan isn't important so far as the SetOp is
+ * concerned, but we must make it look real anyway for the benefit of the
+ * next plan level up.
+ */
+ tlist = generate_setop_tlist(op->colTypes, op->colCollations,
+ 0, false, lpath_tlist, refnames_tlist,
+ &result_trivial_tlist);
+
+ /* We should not have needed any type coercions in the tlist */
+ Assert(result_trivial_tlist);
+
+ *pTargetList = tlist;
+
+ /* Identify the grouping semantics */
+ groupList = generate_setop_grouplist(op, tlist);
+
+ /* Check whether the operators support sorting or hashing */
+ can_sort = grouping_is_sortable(groupList);
+ can_hash = grouping_is_hashable(groupList);
+ if (!can_sort && !can_hash)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ /* translator: %s is INTERSECT or EXCEPT */
+ errmsg("could not implement %s",
+ (op->op == SETOP_INTERSECT) ? "INTERSECT" : "EXCEPT"),
+ errdetail("Some of the datatypes only support hashing, while others only support sorting.")));
+
+ if (can_sort)
+ {
+ /* Determine the pathkeys for sorting by the whole target list */
+ nonunion_pathkeys = make_pathkeys_for_sortclauses(root, groupList,
+ tlist);
+
+ root->query_pathkeys = nonunion_pathkeys;
+ }
+
+ /*
+ * Now that we've got all that info, we can build the child paths.
+ */
+ if (lrel->rtekind == RTE_SUBQUERY)
+ build_setop_child_paths(root, lrel, lpath_trivial_tlist, lpath_tlist,
+ nonunion_pathkeys, &dLeftGroups);
+ else
+ dLeftGroups = lrel->rows;
if (rrel->rtekind == RTE_SUBQUERY)
build_setop_child_paths(root, rrel, rpath_trivial_tlist, rpath_tlist,
- NIL, &dRightGroups);
+ nonunion_pathkeys, &dRightGroups);
else
dRightGroups = rrel->rows;
@@ -1102,30 +1124,11 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
lpath = lrel->cheapest_total_path;
rpath = rrel->cheapest_total_path;
- /*
- * Generate tlist for SetOp plan node.
- *
- * The tlist for a SetOp plan isn't important so far as the SetOp is
- * concerned, but we must make it look real anyway for the benefit of the
- * next plan level up.
- */
- tlist = generate_setop_tlist(op->colTypes, op->colCollations, -1,
- 0, false, lpath_tlist, refnames_tlist,
- &result_trivial_tlist);
-
- /* We should not have needed any type coercions in the tlist */
- Assert(result_trivial_tlist);
-
- *pTargetList = tlist;
-
/* Build result relation. */
result_rel = fetch_upper_rel(root, UPPERREL_SETOP,
bms_union(lrel->relids, rrel->relids));
result_rel->reltarget = create_pathtarget(root, tlist);
- /* Identify the grouping semantics */
- groupList = generate_setop_grouplist(op, tlist);
-
/*
* Estimate number of distinct groups that we'll need hashtable entries
* for; this is the size of the left-hand input for EXCEPT, or the smaller
@@ -1144,66 +1147,106 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root,
dNumGroups = dLeftGroups;
dNumOutputRows = op->all ? Min(lpath->rows, rpath->rows) : dNumGroups;
}
+ result_rel->rows = dNumOutputRows;
+
+ /* Select the SetOpCmd type */
+ switch (op->op)
+ {
+ case SETOP_INTERSECT:
+ cmd = op->all ? SETOPCMD_INTERSECT_ALL : SETOPCMD_INTERSECT;
+ break;
+ case SETOP_EXCEPT:
+ cmd = op->all ? SETOPCMD_EXCEPT_ALL : SETOPCMD_EXCEPT;
+ break;
+ default:
+ elog(ERROR, "unrecognized set op: %d", (int) op->op);
+ cmd = SETOPCMD_INTERSECT; /* keep compiler quiet */
+ break;
+ }
/*
- * Decide whether to hash or sort, and add sort nodes if needed.
+ * If we can hash, that just requires a SetOp atop the cheapest inputs.
*/
- use_hash = choose_hashed_setop(root, groupList, lpath, rpath,
- dNumGroups, dNumOutputRows,
- (op->op == SETOP_INTERSECT) ? "INTERSECT" : "EXCEPT");
+ if (can_hash)
+ {
+ path = (Path *) create_setop_path(root,
+ result_rel,
+ lpath,
+ rpath,
+ cmd,
+ SETOP_HASHED,
+ groupList,
+ dNumGroups,
+ dNumOutputRows);
+ add_path(result_rel, path);
+ }
- if (groupList && !use_hash)
+ /*
+ * If we can sort, generate the cheapest sorted input paths, and add a
+ * SetOp atop those.
+ */
+ if (can_sort)
{
List *pathkeys;
+ Path *slpath,
+ *srpath;
+ /* First the left input ... */
pathkeys = make_pathkeys_for_sortclauses(root,
groupList,
lpath_tlist);
- if (!pathkeys_contained_in(pathkeys, lpath->pathkeys))
- lpath = (Path *) create_sort_path(root,
- lpath->parent,
- lpath,
- pathkeys,
- -1.0);
+ if (pathkeys_contained_in(pathkeys, lpath->pathkeys))
+ slpath = lpath; /* cheapest path is already sorted */
+ else
+ {
+ slpath = get_cheapest_path_for_pathkeys(lrel->pathlist,
+ nonunion_pathkeys,
+ NULL,
+ TOTAL_COST,
+ false);
+ /* Subquery failed to produce any presorted paths? */
+ if (slpath == NULL)
+ slpath = (Path *) create_sort_path(root,
+ lpath->parent,
+ lpath,
+ pathkeys,
+ -1.0);
+ }
+
+ /* and now the same for the right. */
pathkeys = make_pathkeys_for_sortclauses(root,
groupList,
rpath_tlist);
- if (!pathkeys_contained_in(pathkeys, rpath->pathkeys))
- rpath = (Path *) create_sort_path(root,
- rpath->parent,
- rpath,
- pathkeys,
- -1.0);
- }
+ if (pathkeys_contained_in(pathkeys, rpath->pathkeys))
+ srpath = rpath; /* cheapest path is already sorted */
+ else
+ {
+ srpath = get_cheapest_path_for_pathkeys(rrel->pathlist,
+ nonunion_pathkeys,
+ NULL,
+ TOTAL_COST,
+ false);
+ /* Subquery failed to produce any presorted paths? */
+ if (srpath == NULL)
+ srpath = (Path *) create_sort_path(root,
+ rpath->parent,
+ rpath,
+ pathkeys,
+ -1.0);
+ }
- /*
- * Finally, add a SetOp path node to generate the correct output.
- */
- switch (op->op)
- {
- case SETOP_INTERSECT:
- cmd = op->all ? SETOPCMD_INTERSECT_ALL : SETOPCMD_INTERSECT;
- break;
- case SETOP_EXCEPT:
- cmd = op->all ? SETOPCMD_EXCEPT_ALL : SETOPCMD_EXCEPT;
- break;
- default:
- elog(ERROR, "unrecognized set op: %d", (int) op->op);
- cmd = SETOPCMD_INTERSECT; /* keep compiler quiet */
- break;
+ path = (Path *) create_setop_path(root,
+ result_rel,
+ slpath,
+ srpath,
+ cmd,
+ SETOP_SORTED,
+ groupList,
+ dNumGroups,
+ dNumOutputRows);
+ add_path(result_rel, path);
}
- path = (Path *) create_setop_path(root,
- result_rel,
- lpath,
- rpath,
- cmd,
- use_hash ? SETOP_HASHED : SETOP_SORTED,
- groupList,
- dNumGroups,
- dNumOutputRows);
-
- result_rel->rows = path->rows;
- add_path(result_rel, path);
+
return result_rel;
}
@@ -1259,17 +1302,15 @@ plan_union_children(PlannerInfo *root,
/*
* Not same, so plan this child separately.
*
- * Note we disallow any resjunk columns in child results. This is
- * necessary since the Append node that implements the union won't do
- * any projection, and upper levels will get confused if some of our
- * output tuples have junk and some don't. This case only arises when
- * we have an EXCEPT or INTERSECT as child, else there won't be
- * resjunk anyway.
+ * If top_union isn't a UNION ALL, then we are interested in sorted
+ * output from the child, so pass top_union as parentOp. Note that
+ * this isn't necessarily the child node's immediate SetOperationStmt
+ * parent, but that's fine: it's the effective parent.
*/
result = lappend(result, recurse_set_operations(setOp, root,
+ top_union->all ? NULL : top_union,
top_union->colTypes,
top_union->colCollations,
- false, -1,
refnames_tlist,
&child_tlist,
&trivial_tlist));
@@ -1299,120 +1340,10 @@ postprocess_setop_rel(PlannerInfo *root, RelOptInfo *rel)
}
/*
- * choose_hashed_setop - should we use hashing for a set operation?
- *
- * XXX probably this should go away: just make both paths and let
- * add_path sort it out.
- */
-static bool
-choose_hashed_setop(PlannerInfo *root, List *groupClauses,
- Path *lpath, Path *rpath,
- double dNumGroups, double dNumOutputRows,
- const char *construct)
-{
- int numGroupCols = list_length(groupClauses);
- Size hash_mem_limit = get_hash_memory_limit();
- bool can_sort;
- bool can_hash;
- Size hashentrysize;
- Path hashed_p;
- Path sorted_p;
- double tuple_fraction;
-
- /* Check whether the operators support sorting or hashing */
- can_sort = grouping_is_sortable(groupClauses);
- can_hash = grouping_is_hashable(groupClauses);
- if (can_hash && can_sort)
- {
- /* we have a meaningful choice to make, continue ... */
- }
- else if (can_hash)
- return true;
- else if (can_sort)
- return false;
- else
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- /* translator: %s is UNION, INTERSECT, or EXCEPT */
- errmsg("could not implement %s", construct),
- errdetail("Some of the datatypes only support hashing, while others only support sorting.")));
-
- /* Prefer sorting when enable_hashagg is off */
- if (!enable_hashagg)
- return false;
-
- /*
- * Don't do it if it doesn't look like the hashtable will fit into
- * hash_mem.
- */
- hashentrysize = MAXALIGN(lpath->pathtarget->width) + MAXALIGN(SizeofMinimalTupleHeader);
-
- if (hashentrysize * dNumGroups > hash_mem_limit)
- return false;
-
- /*
- * See if the estimated cost is no more than doing it the other way.
- *
- * We need to consider input_plan + hashagg versus input_plan + sort +
- * group. XXX NOT TRUE: Note that the actual result plan might involve a
- * SetOp or Unique node, not Agg or Group, but the cost estimates for Agg
- * and Group should be close enough for our purposes here.
- *
- * These path variables are dummies that just hold cost fields; we don't
- * make actual Paths for these steps.
- */
- cost_agg(&hashed_p, root, AGG_HASHED, NULL,
- numGroupCols, dNumGroups,
- NIL,
- lpath->disabled_nodes + rpath->disabled_nodes,
- lpath->startup_cost + rpath->startup_cost,
- lpath->total_cost + rpath->total_cost,
- lpath->rows + rpath->rows,
- lpath->pathtarget->width);
-
- /*
- * Now for the sorted case. XXX NOT TRUE: Note that the input is *always*
- * unsorted, since it was made by appending unrelated sub-relations
- * together.
- */
- sorted_p.disabled_nodes = lpath->disabled_nodes + rpath->disabled_nodes;
- sorted_p.startup_cost = lpath->startup_cost + rpath->startup_cost;
- sorted_p.total_cost = lpath->total_cost + rpath->total_cost;
- /* XXX cost_sort doesn't actually look at pathkeys, so just pass NIL */
- cost_sort(&sorted_p, root, NIL, sorted_p.disabled_nodes,
- sorted_p.total_cost,
- lpath->rows + rpath->rows,
- lpath->pathtarget->width,
- 0.0, work_mem, -1.0);
- cost_group(&sorted_p, root, numGroupCols, dNumGroups,
- NIL,
- sorted_p.disabled_nodes,
- sorted_p.startup_cost, sorted_p.total_cost,
- lpath->rows + rpath->rows);
-
- /*
- * Now make the decision using the top-level tuple fraction. First we
- * have to convert an absolute count (LIMIT) into fractional form.
- */
- tuple_fraction = root->tuple_fraction;
- if (tuple_fraction >= 1.0)
- tuple_fraction /= dNumOutputRows;
-
- if (compare_fractional_path_costs(&hashed_p, &sorted_p,
- tuple_fraction) < 0)
- {
- /* Hashed is cheaper, so use it */
- return true;
- }
- return false;
-}
-
-/*
* Generate targetlist for a set-operation plan node
*
* colTypes: OID list of set-op's result column datatypes
* colCollations: OID list of set-op's result column collations
- * flag: -1 if no flag column needed, 0 or 1 to create a const flag column
* varno: varno to use in generated Vars
* hack_constants: true to copy up constants (see comments in code)
* input_tlist: targetlist of this node's input node
@@ -1421,7 +1352,6 @@ choose_hashed_setop(PlannerInfo *root, List *groupClauses,
*/
static List *
generate_setop_tlist(List *colTypes, List *colCollations,
- int flag,
Index varno,
bool hack_constants,
List *input_tlist,
@@ -1520,7 +1450,7 @@ generate_setop_tlist(List *colTypes, List *colCollations,
false);
/*
- * By convention, all non-resjunk columns in a setop tree have
+ * By convention, all output columns in a setop tree have
* ressortgroupref equal to their resno. In some cases the ref isn't
* needed, but this is a cleaner way than modifying the tlist later.
*/
@@ -1529,25 +1459,6 @@ generate_setop_tlist(List *colTypes, List *colCollations,
tlist = lappend(tlist, tle);
}
- if (flag >= 0)
- {
- /* Add a resjunk flag column */
- /* flag value is the given constant */
- expr = (Node *) makeConst(INT4OID,
- -1,
- InvalidOid,
- sizeof(int32),
- Int32GetDatum(flag),
- false,
- true);
- tle = makeTargetEntry((Expr *) expr,
- (AttrNumber) resno++,
- pstrdup("flag"),
- true);
- tlist = lappend(tlist, tle);
- *trivial_tlist = false; /* the extra entry makes it not trivial */
- }
-
return tlist;
}
@@ -1556,7 +1467,6 @@ 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
- * flag: true to create a flag column copied up from subplans
* input_tlists: list of tlists for sub-plans of the Append
* refnames_tlist: targetlist to take column names from
*
@@ -1570,7 +1480,6 @@ generate_setop_tlist(List *colTypes, List *colCollations,
*/
static List *
generate_append_tlist(List *colTypes, List *colCollations,
- bool flag,
List *input_tlists,
List *refnames_tlist)
{
@@ -1604,8 +1513,7 @@ generate_append_tlist(List *colTypes, List *colCollations,
{
TargetEntry *subtle = (TargetEntry *) lfirst(subtlistl);
- if (subtle->resjunk)
- continue;
+ Assert(!subtle->resjunk);
Assert(curColType != NULL);
if (exprType((Node *) subtle->expr) == lfirst_oid(curColType))
{
@@ -1654,7 +1562,7 @@ generate_append_tlist(List *colTypes, List *colCollations,
false);
/*
- * By convention, all non-resjunk columns in a setop tree have
+ * By convention, all output columns in a setop tree have
* ressortgroupref equal to their resno. In some cases the ref isn't
* needed, but this is a cleaner way than modifying the tlist later.
*/
@@ -1663,23 +1571,6 @@ generate_append_tlist(List *colTypes, List *colCollations,
tlist = lappend(tlist, tle);
}
- if (flag)
- {
- /* Add a resjunk flag column */
- /* flag value is shown as copied up from subplan */
- expr = (Node *) makeVar(0,
- resno,
- INT4OID,
- -1,
- InvalidOid,
- 0);
- tle = makeTargetEntry((Expr *) expr,
- (AttrNumber) resno++,
- pstrdup("flag"),
- true);
- tlist = lappend(tlist, tle);
- }
-
pfree(colTypmods);
return tlist;
@@ -1709,12 +1600,7 @@ generate_setop_grouplist(SetOperationStmt *op, List *targetlist)
TargetEntry *tle = (TargetEntry *) lfirst(lt);
SortGroupClause *sgc;
- if (tle->resjunk)
- {
- /* resjunk columns should not have sortgrouprefs */
- Assert(tle->ressortgroupref == 0);
- continue; /* ignore resjunk columns */
- }
+ Assert(!tle->resjunk);
/* non-resjunk columns should have sortgroupref = resno */
Assert(tle->ressortgroupref == tle->resno);
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index e52e4b1d677..4f74cafa259 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3681,17 +3681,70 @@ create_setop_path(PlannerInfo *root,
pathnode->numGroups = numGroups;
/*
- * Charge one cpu_operator_cost per comparison per input tuple. We assume
- * all columns get compared at most of the tuples.
- *
- * XXX all wrong for hashing
+ * Compute cost estimates. As things stand, we end up with the same total
+ * cost in this node for sort and hash methods, but different startup
+ * costs. This could be refined perhaps, but it'll do for now.
*/
pathnode->path.disabled_nodes =
leftpath->disabled_nodes + rightpath->disabled_nodes;
- pathnode->path.startup_cost =
- leftpath->startup_cost + rightpath->startup_cost;
- pathnode->path.total_cost = leftpath->total_cost + rightpath->total_cost +
- cpu_operator_cost * (leftpath->rows + rightpath->rows) * list_length(groupList);
+ if (strategy == SETOP_SORTED)
+ {
+ /*
+ * In sorted mode, we can emit output incrementally. Charge one
+ * cpu_operator_cost per comparison per input tuple. Like cost_group,
+ * we assume all columns get compared at most of the tuples.
+ */
+ pathnode->path.startup_cost =
+ leftpath->startup_cost + rightpath->startup_cost;
+ pathnode->path.total_cost =
+ leftpath->total_cost + rightpath->total_cost +
+ cpu_operator_cost * (leftpath->rows + rightpath->rows) * list_length(groupList);
+
+ /*
+ * Also charge a small amount per extracted tuple. Like cost_sort,
+ * charge only operator cost not cpu_tuple_cost, since SetOp does no
+ * qual-checking or projection.
+ */
+ pathnode->path.total_cost += cpu_operator_cost * outputRows;
+ }
+ else
+ {
+ Size hashentrysize;
+
+ /*
+ * In hashed mode, we must read all the input before we can emit
+ * anything. Also charge comparison costs to represent the cost of
+ * hash table lookups.
+ */
+ pathnode->path.startup_cost =
+ leftpath->total_cost + rightpath->total_cost +
+ cpu_operator_cost * (leftpath->rows + rightpath->rows) * list_length(groupList);
+ pathnode->path.total_cost = pathnode->path.startup_cost;
+
+ /*
+ * Also charge a small amount per extracted tuple. Like cost_sort,
+ * charge only operator cost not cpu_tuple_cost, since SetOp does no
+ * qual-checking or projection.
+ */
+ pathnode->path.total_cost += cpu_operator_cost * outputRows;
+
+ /*
+ * Mark the path as disabled if enable_hashagg is off. While this
+ * isn't exactly a HashAgg node, it seems close enough to justify
+ * letting that switch control it.
+ */
+ if (!enable_hashagg)
+ pathnode->path.disabled_nodes++;
+
+ /*
+ * Also disable if it doesn't look like the hashtable will fit into
+ * hash_mem.
+ */
+ hashentrysize = MAXALIGN(leftpath->pathtarget->width) +
+ MAXALIGN(SizeofMinimalTupleHeader);
+ if (hashentrysize * numGroups > get_hash_memory_limit())
+ pathnode->path.disabled_nodes++;
+ }
pathnode->path.rows = outputRows;
return pathnode;
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index a52dec285d5..74bb6c1d3b1 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -53,6 +53,5 @@ extern void preprocess_aggrefs(PlannerInfo *root, Node *clause);
* prototypes for prepunion.c
*/
extern RelOptInfo *plan_set_operations(PlannerInfo *root);
-extern bool set_operation_ordered_results_useful(SetOperationStmt *setop);
#endif /* PREP_H */
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index b4cd204de77..ebc545e2461 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -1221,8 +1221,10 @@ where o.ten = 0;
(1 row)
--
--- Test rescan of a SetOp node
+-- Test rescan of a hashed SetOp node
--
+begin;
+set local enable_sort = off;
explain (costs off)
select count(*) from
onek o cross join lateral (
@@ -1256,6 +1258,50 @@ where o.ten = 1;
100
(1 row)
+rollback;
+--
+-- Test rescan of a sorted SetOp node
+--
+begin;
+set local enable_hashagg = off;
+explain (costs off)
+select count(*) from
+ onek o cross join lateral (
+ select * from onek i1 where i1.unique1 = o.unique1
+ except
+ select * from onek i2 where i2.unique1 = o.unique2
+ ) ss
+where o.ten = 1;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Aggregate
+ -> Nested Loop
+ -> Seq Scan on onek o
+ Filter: (ten = 1)
+ -> SetOp Except
+ -> Sort
+ Sort Key: i1.unique1, i1.unique2, i1.two, i1.four, i1.ten, i1.twenty, i1.hundred, i1.thousand, i1.twothousand, i1.fivethous, i1.tenthous, i1.odd, i1.even, i1.stringu1, i1.stringu2, i1.string4
+ -> Index Scan using onek_unique1 on onek i1
+ Index Cond: (unique1 = o.unique1)
+ -> Sort
+ Sort Key: i2.unique1, i2.unique2, i2.two, i2.four, i2.ten, i2.twenty, i2.hundred, i2.thousand, i2.twothousand, i2.fivethous, i2.tenthous, i2.odd, i2.even, i2.stringu1, i2.stringu2, i2.string4
+ -> Index Scan using onek_unique1 on onek i2
+ Index Cond: (unique1 = o.unique2)
+(13 rows)
+
+select count(*) from
+ onek o cross join lateral (
+ select * from onek i1 where i1.unique1 = o.unique1
+ except
+ select * from onek i2 where i2.unique1 = o.unique2
+ ) ss
+where o.ten = 1;
+ count
+-------
+ 100
+(1 row)
+
+rollback;
--
-- Test rescan of a RecursiveUnion node
--
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 17a0dea7195..caa8fe70a05 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -385,13 +385,15 @@ select count(*) from
5000
(1 row)
+-- this query will prefer a sorted setop unless we force it.
+set enable_indexscan to off;
explain (costs off)
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
- QUERY PLAN
-------------------------------------------------------------
+ QUERY PLAN
+---------------------------------
HashSetOp Except
- -> Index Only Scan using tenk1_unique1 on tenk1
- -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1
+ -> Seq Scan on tenk1
+ -> Seq Scan on tenk1 tenk1_1
Filter: (unique2 <> 10)
(4 rows)
@@ -401,6 +403,7 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
10
(1 row)
+reset enable_indexscan;
-- the hashed implementation is sensitive to child plans' tuple slot types
explain (costs off)
select * from int8_tbl intersect select q2, q1 from int8_tbl order by 1, 2;
@@ -455,17 +458,15 @@ select count(*) from
explain (costs off)
select count(*) from
( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
- QUERY PLAN
-------------------------------------------------------------------------
+ QUERY PLAN
+------------------------------------------------------------------
Aggregate
-> SetOp Intersect
-> Sort
Sort Key: tenk1.fivethous
-> Seq Scan on tenk1
- -> Sort
- Sort Key: tenk1_1.unique1
- -> Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+ -> Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
+(6 rows)
select count(*) from
( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -476,17 +477,13 @@ select count(*) from
explain (costs off)
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
- QUERY PLAN
-------------------------------------------------------------------
+ QUERY PLAN
+------------------------------------------------------------
SetOp Except
- -> Sort
- Sort Key: tenk1.unique1
- -> Index Only Scan using tenk1_unique1 on tenk1
- -> Sort
- Sort Key: tenk1_1.unique2
- -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1
- Filter: (unique2 <> 10)
-(8 rows)
+ -> Index Only Scan using tenk1_unique1 on tenk1
+ -> Index Only Scan using tenk1_unique2 on tenk1 tenk1_1
+ Filter: (unique2 <> 10)
+(4 rows)
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
unique1
@@ -494,6 +491,20 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
10
(1 row)
+explain (costs off)
+select f1 from int4_tbl union all
+ (select unique1 from tenk1 union select unique2 from tenk1);
+ QUERY PLAN
+------------------------------------------------------------------------
+ Append
+ -> Seq Scan on int4_tbl
+ -> 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
+(7 rows)
+
reset enable_hashagg;
-- non-hashable type
set enable_hashagg to on;
@@ -978,7 +989,7 @@ explain (costs off)
select from generate_series(1,5) intersect select from generate_series(1,3);
QUERY PLAN
----------------------------------------------------------
- HashSetOp Intersect
+ SetOp Intersect
-> Function Scan on generate_series
-> Function Scan on generate_series generate_series_1
(3 rows)
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index a6ab42454f0..6ed3636a9e4 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -638,8 +638,11 @@ select sum(ss.tst::int) from
where o.ten = 0;
--
--- Test rescan of a SetOp node
+-- Test rescan of a hashed SetOp node
--
+begin;
+set local enable_sort = off;
+
explain (costs off)
select count(*) from
onek o cross join lateral (
@@ -657,6 +660,33 @@ select count(*) from
) ss
where o.ten = 1;
+rollback;
+
+--
+-- Test rescan of a sorted SetOp node
+--
+begin;
+set local enable_hashagg = off;
+
+explain (costs off)
+select count(*) from
+ onek o cross join lateral (
+ select * from onek i1 where i1.unique1 = o.unique1
+ except
+ select * from onek i2 where i2.unique1 = o.unique2
+ ) ss
+where o.ten = 1;
+
+select count(*) from
+ onek o cross join lateral (
+ select * from onek i1 where i1.unique1 = o.unique1
+ except
+ select * from onek i2 where i2.unique1 = o.unique2
+ ) ss
+where o.ten = 1;
+
+rollback;
+
--
-- Test rescan of a RecursiveUnion node
--
diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql
index e34e8a2d7e6..13700a6bfc4 100644
--- a/src/test/regress/sql/union.sql
+++ b/src/test/regress/sql/union.sql
@@ -134,10 +134,15 @@ select count(*) from
select count(*) from
( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
+-- this query will prefer a sorted setop unless we force it.
+set enable_indexscan to off;
+
explain (costs off)
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
+reset enable_indexscan;
+
-- the hashed implementation is sensitive to child plans' tuple slot types
explain (costs off)
select * from int8_tbl intersect select q2, q1 from int8_tbl order by 1, 2;
@@ -162,6 +167,10 @@ explain (costs off)
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
+explain (costs off)
+select f1 from int4_tbl union all
+ (select unique1 from tenk1 union select unique2 from tenk1);
+
reset enable_hashagg;
-- non-hashable type