Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 8b9d323

Browse files
committed
Refactor planning of projection steps that don't need a Result plan node.
The original upper-planner-pathification design (commit 3fc6e2d) assumed that we could always determine during Path formation whether or not we would need a Result plan node to perform projection of a targetlist. That turns out not to work very well, though, because createplan.c still has some responsibilities for choosing the specific target list associated with sorting/grouping nodes (in particular it might choose to add resjunk columns for sorting). We might not ever refactor that --- doing so would push more work into Path formation, which isn't attractive --- and we certainly won't do so for 9.6. So, while create_projection_path and apply_projection_to_path can tell for sure what will happen if the subpath is projection-capable, they can't tell for sure when it isn't. This is at least a latent bug in apply_projection_to_path, which might think it can apply a target to a non-projecting node when the node will end up computing something different. Also, I'd tied the creation of a ProjectionPath node to whether or not a Result is needed, but it turns out that we sometimes need a ProjectionPath node anyway to avoid modifying a possibly-shared subpath node. Callers had to use create_projection_path for such cases, and we added code to them that knew about the potential omission of a Result node and attempted to adjust the cost estimates for that. That was uncertainly correct and definitely ugly/unmaintainable. To fix, have create_projection_path explicitly check whether a Result is needed and adjust its cost estimate accordingly, though it creates a ProjectionPath in either case. apply_projection_to_path is now mostly just an optimized version that can avoid creating an extra Path node when the input is known to not be shared with any other live path. (There is one case that create_projection_path doesn't handle, which is pushing parallel-safe expressions below a Gather node. We could make it do that by duplicating the GatherPath, but there seems no need as yet.) create_projection_plan still has to recheck the tlist-match condition, which means that if the matching situation does get changed by createplan.c then we'll have made a slightly incorrect cost estimate. But there seems no help for that in the near term, and I doubt it occurs often enough, let alone would change planning decisions often enough, to be worth stressing about. I added a "dummypp" field to ProjectionPath to track whether create_projection_path thinks a Result is needed. This is not really necessary as-committed because create_projection_plan doesn't look at the flag; but it seems like a good idea to remember what we thought when forming the cost estimate, if only for debugging purposes. In passing, get rid of the target_parallel parameter added to apply_projection_to_path by commit 54f5c51. I don't think that's a good idea because it involves callers in what should be an internal decision, and opens us up to missing optimization opportunities if callers think they don't need to provide a valid flag, as most don't. For the moment, this just costs us an extra has_parallel_hazard call when planning a Gather. If that starts to look expensive, I think a better solution would be to teach PathTarget to carry/cache knowledge of parallel-safety of its contents.
1 parent 936b62d commit 8b9d323

File tree

8 files changed

+110
-101
lines changed

8 files changed

+110
-101
lines changed

src/backend/nodes/outfuncs.c

+1
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,7 @@ _outProjectionPath(StringInfo str, const ProjectionPath *node)
18091809
_outPathInfo(str, (const Path *) node);
18101810

18111811
WRITE_NODE_FIELD(subpath);
1812+
WRITE_BOOL_FIELD(dummypp);
18121813
}
18131814

18141815
static void

src/backend/optimizer/plan/createplan.c

+21-14
Original file line numberDiff line numberDiff line change
@@ -1409,8 +1409,9 @@ create_gather_plan(PlannerInfo *root, GatherPath *best_path)
14091409
/*
14101410
* create_projection_plan
14111411
*
1412-
* Create a Result node to do a projection step and (recursively) plans
1413-
* for its subpaths.
1412+
* Create a plan tree to do a projection step and (recursively) plans
1413+
* for its subpaths. We may need a Result node for the projection,
1414+
* but sometimes we can just let the subplan do the work.
14141415
*/
14151416
static Plan *
14161417
create_projection_plan(PlannerInfo *root, ProjectionPath *best_path)
@@ -1425,31 +1426,37 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path)
14251426
tlist = build_path_tlist(root, &best_path->path);
14261427

14271428
/*
1428-
* We might not really need a Result node here. There are several ways
1429-
* that this can happen. For example, MergeAppend doesn't project, so we
1430-
* would have thought that we needed a projection to attach resjunk sort
1431-
* columns to its output ... but create_merge_append_plan might have added
1432-
* those same resjunk sort columns to both MergeAppend and its children.
1433-
* Alternatively, apply_projection_to_path might have created a projection
1434-
* path as the subpath of a Gather node even though the subpath was
1435-
* projection-capable. So, if the subpath is capable of projection or the
1436-
* desired tlist is the same expression-wise as the subplan's, just jam it
1437-
* in there. We'll have charged for a Result that doesn't actually appear
1438-
* in the plan, but that's better than having a Result we don't need.
1429+
* We might not really need a Result node here, either because the subplan
1430+
* can project or because it's returning the right list of expressions
1431+
* anyway. Usually create_projection_path will have detected that and set
1432+
* dummypp if we don't need a Result; but its decision can't be final,
1433+
* because some createplan.c routines change the tlists of their nodes.
1434+
* (An example is that create_merge_append_plan might add resjunk sort
1435+
* columns to a MergeAppend.) So we have to recheck here. If we do
1436+
* arrive at a different answer than create_projection_path did, we'll
1437+
* have made slightly wrong cost estimates; but label the plan with the
1438+
* cost estimates we actually used, not "corrected" ones. (XXX this could
1439+
* be cleaned up if we moved more of the sortcolumn setup logic into Path
1440+
* creation, but that would add expense to creating Paths we might end up
1441+
* not using.)
14391442
*/
14401443
if (is_projection_capable_path(best_path->subpath) ||
14411444
tlist_same_exprs(tlist, subplan->targetlist))
14421445
{
1446+
/* Don't need a separate Result, just assign tlist to subplan */
14431447
plan = subplan;
14441448
plan->targetlist = tlist;
14451449

1446-
/* Adjust cost to match what we thought during planning */
1450+
/* Label plan with the estimated costs we actually used */
14471451
plan->startup_cost = best_path->path.startup_cost;
14481452
plan->total_cost = best_path->path.total_cost;
1453+
plan->plan_rows = best_path->path.rows;
1454+
plan->plan_width = best_path->path.pathtarget->width;
14491455
/* ... but be careful not to munge subplan's parallel-aware flag */
14501456
}
14511457
else
14521458
{
1459+
/* We need a Result node */
14531460
plan = (Plan *) make_result(tlist, NULL, subplan);
14541461

14551462
copy_generic_path_info(plan, (Path *) best_path);

src/backend/optimizer/plan/planagg.c

+1-2
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,7 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo,
465465
* cheapest path.)
466466
*/
467467
sorted_path = apply_projection_to_path(subroot, final_rel, sorted_path,
468-
create_pathtarget(subroot, tlist),
469-
false);
468+
create_pathtarget(subroot, tlist));
470469

471470
/*
472471
* Determine cost to get just the first row of the presorted path.

src/backend/optimizer/plan/planner.c

+10-44
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,6 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
15001500
PathTarget *grouping_target;
15011501
PathTarget *scanjoin_target;
15021502
bool have_grouping;
1503-
bool scanjoin_target_parallel_safe = false;
15041503
WindowFuncLists *wflists = NULL;
15051504
List *activeWindows = NIL;
15061505
List *rollup_lists = NIL;
@@ -1730,14 +1729,6 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
17301729
else
17311730
scanjoin_target = grouping_target;
17321731

1733-
/*
1734-
* Check whether scan/join target is parallel safe ... unless there
1735-
* are no partial paths, in which case we don't care.
1736-
*/
1737-
if (current_rel->partial_pathlist &&
1738-
!has_parallel_hazard((Node *) scanjoin_target->exprs, false))
1739-
scanjoin_target_parallel_safe = true;
1740-
17411732
/*
17421733
* Forcibly apply scan/join target to all the Paths for the scan/join
17431734
* rel.
@@ -1756,8 +1747,7 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
17561747

17571748
Assert(subpath->param_info == NULL);
17581749
path = apply_projection_to_path(root, current_rel,
1759-
subpath, scanjoin_target,
1760-
scanjoin_target_parallel_safe);
1750+
subpath, scanjoin_target);
17611751
/* If we had to add a Result, path is different from subpath */
17621752
if (path != subpath)
17631753
{
@@ -1774,15 +1764,13 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
17741764
* partial pathlist will expect partial paths for that rel to produce
17751765
* the same output as complete paths ... and we just changed the
17761766
* output for the complete paths, so we'll need to do the same thing
1777-
* for partial paths.
1767+
* for partial paths. But only parallel-safe expressions can be
1768+
* computed by partial paths.
17781769
*/
1779-
if (scanjoin_target_parallel_safe)
1770+
if (current_rel->partial_pathlist &&
1771+
!has_parallel_hazard((Node *) scanjoin_target->exprs, false))
17801772
{
1781-
/*
1782-
* Apply the scan/join target to each partial path. Otherwise,
1783-
* anything that attempts to use the partial paths for further
1784-
* upper planning may go wrong.
1785-
*/
1773+
/* Apply the scan/join target to each partial path */
17861774
foreach(lc, current_rel->partial_pathlist)
17871775
{
17881776
Path *subpath = (Path *) lfirst(lc);
@@ -1792,36 +1780,14 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
17921780
Assert(subpath->param_info == NULL);
17931781

17941782
/*
1795-
* We can't use apply_projection_to_path() here, because there
1796-
* could already be pointers to these paths, and therefore we
1797-
* dare not modify them in place. Instead, we must use
1798-
* create_projection_path() unconditionally.
1783+
* Don't use apply_projection_to_path() here, because there
1784+
* could be other pointers to these paths, and therefore we
1785+
* mustn't modify them in place.
17991786
*/
18001787
newpath = (Path *) create_projection_path(root,
18011788
current_rel,
18021789
subpath,
18031790
scanjoin_target);
1804-
1805-
/*
1806-
* Although create_projection_path() inserts a ProjectionPath
1807-
* unconditionally, create_projection_plan() will only insert
1808-
* a Result node if the subpath is not projection-capable, so
1809-
* we should discount the cost of that node if it will not
1810-
* actually get inserted. (This is pretty grotty but we can
1811-
* improve it later if it seems important.)
1812-
*/
1813-
if (equal(scanjoin_target->exprs, subpath->pathtarget->exprs))
1814-
{
1815-
/* at most we need a relabeling of the subpath */
1816-
newpath->startup_cost = subpath->startup_cost;
1817-
newpath->total_cost = subpath->total_cost;
1818-
}
1819-
else if (is_projection_capable_path(subpath))
1820-
{
1821-
/* need to project, but we don't need a Result */
1822-
newpath->total_cost -= cpu_tuple_cost * subpath->rows;
1823-
}
1824-
18251791
lfirst(lc) = newpath;
18261792
}
18271793
}
@@ -4231,7 +4197,7 @@ create_ordered_paths(PlannerInfo *root,
42314197
/* Add projection step if needed */
42324198
if (path->pathtarget != target)
42334199
path = apply_projection_to_path(root, ordered_rel,
4234-
path, target, false);
4200+
path, target);
42354201

42364202
add_path(ordered_rel, path);
42374203
}

src/backend/optimizer/prep/prepunion.c

+2-4
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
325325
refnames_tlist);
326326

327327
path = apply_projection_to_path(root, rel, path,
328-
create_pathtarget(root, tlist),
329-
false);
328+
create_pathtarget(root, tlist));
330329

331330
/* Return the fully-fledged tlist to caller, too */
332331
*pTargetList = tlist;
@@ -395,8 +394,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
395394
path->parent,
396395
path,
397396
create_pathtarget(root,
398-
*pTargetList),
399-
false);
397+
*pTargetList));
400398
}
401399
return path;
402400
}

src/backend/optimizer/util/pathnode.c

+63-31
Original file line numberDiff line numberDiff line change
@@ -2168,6 +2168,7 @@ create_projection_path(PlannerInfo *root,
21682168
PathTarget *target)
21692169
{
21702170
ProjectionPath *pathnode = makeNode(ProjectionPath);
2171+
PathTarget *oldtarget = subpath->pathtarget;
21712172

21722173
pathnode->path.pathtype = T_Result;
21732174
pathnode->path.parent = rel;
@@ -2184,13 +2185,46 @@ create_projection_path(PlannerInfo *root,
21842185
pathnode->subpath = subpath;
21852186

21862187
/*
2187-
* The Result node's cost is cpu_tuple_cost per row, plus the cost of
2188-
* evaluating the tlist. There is no qual to worry about.
2188+
* We might not need a separate Result node. If the input plan node type
2189+
* can project, we can just tell it to project something else. Or, if it
2190+
* can't project but the desired target has the same expression list as
2191+
* what the input will produce anyway, we can still give it the desired
2192+
* tlist (possibly changing its ressortgroupref labels, but nothing else).
2193+
* Note: in the latter case, create_projection_plan has to recheck our
2194+
* conclusion; see comments therein.
21892195
*/
2190-
pathnode->path.rows = subpath->rows;
2191-
pathnode->path.startup_cost = subpath->startup_cost + target->cost.startup;
2192-
pathnode->path.total_cost = subpath->total_cost + target->cost.startup +
2193-
(cpu_tuple_cost + target->cost.per_tuple) * subpath->rows;
2196+
if (is_projection_capable_path(subpath) ||
2197+
equal(oldtarget->exprs, target->exprs))
2198+
{
2199+
/* No separate Result node needed */
2200+
pathnode->dummypp = true;
2201+
2202+
/*
2203+
* Set cost of plan as subpath's cost, adjusted for tlist replacement.
2204+
*/
2205+
pathnode->path.rows = subpath->rows;
2206+
pathnode->path.startup_cost = subpath->startup_cost +
2207+
(target->cost.startup - oldtarget->cost.startup);
2208+
pathnode->path.total_cost = subpath->total_cost +
2209+
(target->cost.startup - oldtarget->cost.startup) +
2210+
(target->cost.per_tuple - oldtarget->cost.per_tuple) * subpath->rows;
2211+
}
2212+
else
2213+
{
2214+
/* We really do need the Result node */
2215+
pathnode->dummypp = false;
2216+
2217+
/*
2218+
* The Result node's cost is cpu_tuple_cost per row, plus the cost of
2219+
* evaluating the tlist. There is no qual to worry about.
2220+
*/
2221+
pathnode->path.rows = subpath->rows;
2222+
pathnode->path.startup_cost = subpath->startup_cost +
2223+
target->cost.startup;
2224+
pathnode->path.total_cost = subpath->total_cost +
2225+
target->cost.startup +
2226+
(cpu_tuple_cost + target->cost.per_tuple) * subpath->rows;
2227+
}
21942228

21952229
return pathnode;
21962230
}
@@ -2199,38 +2233,37 @@ create_projection_path(PlannerInfo *root,
21992233
* apply_projection_to_path
22002234
* Add a projection step, or just apply the target directly to given path.
22012235
*
2202-
* Most plan types include ExecProject, so we can implement a new projection
2203-
* without an extra plan node: just replace the given path's pathtarget with
2204-
* the desired one. If the given path can't project, add a ProjectionPath.
2236+
* This has the same net effect as create_projection_path(), except that if
2237+
* a separate Result plan node isn't needed, we just replace the given path's
2238+
* pathtarget with the desired one. This must be used only when the caller
2239+
* knows that the given path isn't referenced elsewhere and so can be modified
2240+
* in-place.
22052241
*
2206-
* We can also short-circuit cases where the targetlist expressions are
2207-
* actually equal; this is not an uncommon case, since it may arise from
2208-
* trying to apply a PathTarget with sortgroupref labeling to a derived
2209-
* path without such labeling.
2242+
* If the input path is a GatherPath, we try to push the new target down to
2243+
* its input as well; this is a yet more invasive modification of the input
2244+
* path, which create_projection_path() can't do.
22102245
*
2211-
* This requires knowing that the source path won't be referenced for other
2212-
* purposes (e.g., other possible paths), since we modify it in-place. Note
2213-
* also that we mustn't change the source path's parent link; so when it is
2246+
* Note that we mustn't change the source path's parent link; so when it is
22142247
* add_path'd to "rel" things will be a bit inconsistent. So far that has
22152248
* not caused any trouble.
22162249
*
22172250
* 'rel' is the parent relation associated with the result
22182251
* 'path' is the path representing the source of data
22192252
* 'target' is the PathTarget to be computed
2220-
* 'target_parallel' indicates that target expressions are all parallel-safe
22212253
*/
22222254
Path *
22232255
apply_projection_to_path(PlannerInfo *root,
22242256
RelOptInfo *rel,
22252257
Path *path,
2226-
PathTarget *target,
2227-
bool target_parallel)
2258+
PathTarget *target)
22282259
{
22292260
QualCost oldcost;
22302261

2231-
/* Make a separate ProjectionPath if needed */
2232-
if (!is_projection_capable_path(path) &&
2233-
!equal(path->pathtarget->exprs, target->exprs))
2262+
/*
2263+
* If given path can't project, we might need a Result node, so make a
2264+
* separate ProjectionPath.
2265+
*/
2266+
if (!is_projection_capable_path(path))
22342267
return (Path *) create_projection_path(root, rel, path, target);
22352268

22362269
/*
@@ -2247,25 +2280,24 @@ apply_projection_to_path(PlannerInfo *root,
22472280
/*
22482281
* If the path happens to be a Gather path, we'd like to arrange for the
22492282
* subpath to return the required target list so that workers can help
2250-
* project. But if there is something that is not parallel-safe in the
2283+
* project. But if there is something that is not parallel-safe in the
22512284
* target expressions, then we can't.
22522285
*/
2253-
if (IsA(path, GatherPath) &&target_parallel)
2286+
if (IsA(path, GatherPath) &&
2287+
!has_parallel_hazard((Node *) target->exprs, false))
22542288
{
22552289
GatherPath *gpath = (GatherPath *) path;
22562290

22572291
/*
22582292
* We always use create_projection_path here, even if the subpath is
22592293
* projection-capable, so as to avoid modifying the subpath in place.
22602294
* It seems unlikely at present that there could be any other
2261-
* references to the subpath anyway, but better safe than sorry.
2262-
* (create_projection_plan will only insert a Result node if the
2263-
* subpath is not projection-capable, so we only include the cost of
2264-
* that node if it will actually be inserted. This is a bit grotty
2265-
* but we can improve it later if it seems important.)
2295+
* references to the subpath, but better safe than sorry.
2296+
*
2297+
* Note that we don't change the GatherPath's cost estimates; it might
2298+
* be appropriate to do so, to reflect the fact that the bulk of the
2299+
* target evaluation will happen in workers.
22662300
*/
2267-
if (!is_projection_capable_path(gpath->subpath))
2268-
gpath->path.total_cost += cpu_tuple_cost * gpath->subpath->rows;
22692301
gpath->subpath = (Path *)
22702302
create_projection_path(root,
22712303
gpath->subpath->parent,

src/include/nodes/relation.h

+11-4
Original file line numberDiff line numberDiff line change
@@ -1274,15 +1274,22 @@ typedef struct HashPath
12741274
/*
12751275
* ProjectionPath represents a projection (that is, targetlist computation)
12761276
*
1277-
* This path node represents using a Result plan node to do a projection.
1278-
* It's only needed atop a node that doesn't support projection (such as
1279-
* Sort); otherwise we just jam the new desired PathTarget into the lower
1280-
* path node, and adjust that node's estimated cost accordingly.
1277+
* Nominally, this path node represents using a Result plan node to do a
1278+
* projection step. However, if the input plan node supports projection,
1279+
* we can just modify its output targetlist to do the required calculations
1280+
* directly, and not need a Result. In some places in the planner we can just
1281+
* jam the desired PathTarget into the input path node (and adjust its cost
1282+
* accordingly), so we don't need a ProjectionPath. But in other places
1283+
* it's necessary to not modify the input path node, so we need a separate
1284+
* ProjectionPath node, which is marked dummy to indicate that we intend to
1285+
* assign the work to the input plan node. The estimated cost for the
1286+
* ProjectionPath node will account for whether a Result will be used or not.
12811287
*/
12821288
typedef struct ProjectionPath
12831289
{
12841290
Path path;
12851291
Path *subpath; /* path representing input source */
1292+
bool dummypp; /* true if no separate Result is needed */
12861293
} ProjectionPath;
12871294

12881295
/*

src/include/optimizer/pathnode.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,7 @@ extern ProjectionPath *create_projection_path(PlannerInfo *root,
143143
extern Path *apply_projection_to_path(PlannerInfo *root,
144144
RelOptInfo *rel,
145145
Path *path,
146-
PathTarget *target,
147-
bool target_parallel);
146+
PathTarget *target);
148147
extern SortPath *create_sort_path(PlannerInfo *root,
149148
RelOptInfo *rel,
150149
Path *subpath,

0 commit comments

Comments
 (0)