summaryrefslogtreecommitdiff
path: root/src/backend/optimizer/util/pathnode.c
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2022-07-19 11:18:19 -0400
committerTom Lane <tgl@sss.pgh.pa.us>2022-07-19 11:18:19 -0400
commite2f6c307c02924e6ee1667890b56280ab1960d2e (patch)
tree37fface37170a61d10cb7df3b6b23e691c298445 /src/backend/optimizer/util/pathnode.c
parent1679d57a550530ebef624738cc1b12647714fca6 (diff)
Estimate cost of elided SubqueryScan, Append, MergeAppend nodes better.
setrefs.c contains logic to discard no-op SubqueryScan nodes, that is, ones that have no qual to check and copy the input targetlist unchanged. (Formally it's not very nice to be applying such optimizations so late in the planner, but there are practical reasons for it; mostly that we can't unify relids between the subquery and the parent query until we flatten the rangetable during setrefs.c.) This behavior falsifies our previous cost estimates, since we would've charged cpu_tuple_cost per row just to pass data through the node. Most of the time that's little enough to not matter, but there are cases where this effect visibly changes the plan compared to what you would've gotten with no sub-select. To improve the situation, make the callers of cost_subqueryscan tell it whether they think the targetlist is trivial. cost_subqueryscan already has the qual list, so it can check the other half of the condition easily. It could make its own determination of tlist triviality too, but doing so would be repetitive (for callers that may call it several times) or unnecessarily expensive (for callers that can determine this more cheaply than a general test would do). This isn't a 100% solution, because createplan.c also does things that can falsify any earlier estimate of whether the tlist is trivial. However, it fixes nearly all cases in practice, if results for the regression tests are anything to go by. setrefs.c also contains logic to discard no-op Append and MergeAppend nodes. We did have knowledge of that behavior at costing time, but somebody failed to update it when a check on parallel-awareness was added to the setrefs.c logic. Fix that while we're here. These changes result in two minor changes in query plans shown in our regression tests. Neither is relevant to the purposes of its test case AFAICT. Patch by me; thanks to Richard Guo for review. Discussion: https://postgr.es/m/2581077.1651703520@sss.pgh.pa.us
Diffstat (limited to 'src/backend/optimizer/util/pathnode.c')
-rw-r--r--src/backend/optimizer/util/pathnode.c56
1 files changed, 44 insertions, 12 deletions
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 483c4f41373..dd64b460865 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1326,19 +1326,28 @@ create_append_path(PlannerInfo *root,
Assert(!parallel_aware || pathnode->path.parallel_safe);
/*
- * If there's exactly one child path, the Append is a no-op and will be
- * discarded later (in setrefs.c); therefore, we can inherit the child's
- * size and cost, as well as its pathkeys if any (overriding whatever the
- * caller might've said). Otherwise, we must do the normal costsize
+ * If there's exactly one child path then the output of the Append is
+ * necessarily ordered the same as the child's, so we can inherit the
+ * child's pathkeys if any, overriding whatever the caller might've said.
+ * Furthermore, if the child's parallel awareness matches the Append's,
+ * then the Append is a no-op and will be discarded later (in setrefs.c).
+ * Then we can inherit the child's size and cost too, effectively charging
+ * zero for the Append. Otherwise, we must do the normal costsize
* calculation.
*/
if (list_length(pathnode->subpaths) == 1)
{
Path *child = (Path *) linitial(pathnode->subpaths);
- pathnode->path.rows = child->rows;
- pathnode->path.startup_cost = child->startup_cost;
- pathnode->path.total_cost = child->total_cost;
+ if (child->parallel_aware == parallel_aware)
+ {
+ pathnode->path.rows = child->rows;
+ pathnode->path.startup_cost = child->startup_cost;
+ pathnode->path.total_cost = child->total_cost;
+ }
+ else
+ cost_append(pathnode, root);
+ /* Must do this last, else cost_append complains */
pathnode->path.pathkeys = child->pathkeys;
}
else
@@ -1476,10 +1485,13 @@ create_merge_append_path(PlannerInfo *root,
/*
* Now we can compute total costs of the MergeAppend. If there's exactly
- * one child path, the MergeAppend is a no-op and will be discarded later
- * (in setrefs.c); otherwise we do the normal cost calculation.
+ * one child path and its parallel awareness matches that of the
+ * MergeAppend, then the MergeAppend is a no-op and will be discarded
+ * later (in setrefs.c); otherwise we do the normal cost calculation.
*/
- if (list_length(subpaths) == 1)
+ if (list_length(subpaths) == 1 &&
+ ((Path *) linitial(subpaths))->parallel_aware ==
+ pathnode->path.parallel_aware)
{
pathnode->path.startup_cost = input_startup_cost;
pathnode->path.total_cost = input_total_cost;
@@ -1986,9 +1998,15 @@ create_gather_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
* create_subqueryscan_path
* Creates a path corresponding to a scan of a subquery,
* returning the pathnode.
+ *
+ * Caller must pass trivial_pathtarget = true if it believes rel->reltarget to
+ * be trivial, ie just a fetch of all the subquery output columns in order.
+ * While we could determine that here, the caller can usually do it more
+ * efficiently (or at least amortize it over multiple calls).
*/
SubqueryScanPath *
create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
+ bool trivial_pathtarget,
List *pathkeys, Relids required_outer)
{
SubqueryScanPath *pathnode = makeNode(SubqueryScanPath);
@@ -2005,7 +2023,8 @@ create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
pathnode->path.pathkeys = pathkeys;
pathnode->subpath = subpath;
- cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info);
+ cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info,
+ trivial_pathtarget);
return pathnode;
}
@@ -3901,10 +3920,23 @@ reparameterize_path(PlannerInfo *root, Path *path,
case T_SubqueryScan:
{
SubqueryScanPath *spath = (SubqueryScanPath *) path;
+ Path *subpath = spath->subpath;
+ bool trivial_pathtarget;
+
+ /*
+ * If existing node has zero extra cost, we must have decided
+ * its target is trivial. (The converse is not true, because
+ * it might have a trivial target but quals to enforce; but in
+ * that case the new node will too, so it doesn't matter
+ * whether we get the right answer here.)
+ */
+ trivial_pathtarget =
+ (subpath->total_cost == spath->path.total_cost);
return (Path *) create_subqueryscan_path(root,
rel,
- spath->subpath,
+ subpath,
+ trivial_pathtarget,
spath->path.pathkeys,
required_outer);
}