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

Commit e2f6c30

Browse files
committed
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
1 parent 1679d57 commit e2f6c30

File tree

9 files changed

+147
-39
lines changed

9 files changed

+147
-39
lines changed

src/backend/optimizer/path/allpaths.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2451,6 +2451,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
24512451
{
24522452
Query *parse = root->parse;
24532453
Query *subquery = rte->subquery;
2454+
bool trivial_pathtarget;
24542455
Relids required_outer;
24552456
pushdown_safety_info safetyInfo;
24562457
double tuple_fraction;
@@ -2613,6 +2614,36 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
26132614
*/
26142615
set_subquery_size_estimates(root, rel);
26152616

2617+
/*
2618+
* Also detect whether the reltarget is trivial, so that we can pass that
2619+
* info to cost_subqueryscan (rather than re-deriving it multiple times).
2620+
* It's trivial if it fetches all the subplan output columns in order.
2621+
*/
2622+
if (list_length(rel->reltarget->exprs) != list_length(subquery->targetList))
2623+
trivial_pathtarget = false;
2624+
else
2625+
{
2626+
trivial_pathtarget = true;
2627+
foreach(lc, rel->reltarget->exprs)
2628+
{
2629+
Node *node = (Node *) lfirst(lc);
2630+
Var *var;
2631+
2632+
if (!IsA(node, Var))
2633+
{
2634+
trivial_pathtarget = false;
2635+
break;
2636+
}
2637+
var = (Var *) node;
2638+
if (var->varno != rti ||
2639+
var->varattno != foreach_current_index(lc) + 1)
2640+
{
2641+
trivial_pathtarget = false;
2642+
break;
2643+
}
2644+
}
2645+
}
2646+
26162647
/*
26172648
* For each Path that subquery_planner produced, make a SubqueryScanPath
26182649
* in the outer query.
@@ -2631,6 +2662,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
26312662
/* Generate outer path using this subpath */
26322663
add_path(rel, (Path *)
26332664
create_subqueryscan_path(root, rel, subpath,
2665+
trivial_pathtarget,
26342666
pathkeys, required_outer));
26352667
}
26362668

@@ -2656,6 +2688,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
26562688
/* Generate outer path using this subpath */
26572689
add_partial_path(rel, (Path *)
26582690
create_subqueryscan_path(root, rel, subpath,
2691+
trivial_pathtarget,
26592692
pathkeys,
26602693
required_outer));
26612694
}

src/backend/optimizer/path/costsize.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1415,10 +1415,12 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
14151415
*
14161416
* 'baserel' is the relation to be scanned
14171417
* 'param_info' is the ParamPathInfo if this is a parameterized path, else NULL
1418+
* 'trivial_pathtarget' is true if the pathtarget is believed to be trivial.
14181419
*/
14191420
void
14201421
cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
1421-
RelOptInfo *baserel, ParamPathInfo *param_info)
1422+
RelOptInfo *baserel, ParamPathInfo *param_info,
1423+
bool trivial_pathtarget)
14221424
{
14231425
Cost startup_cost;
14241426
Cost run_cost;
@@ -1458,6 +1460,22 @@ cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
14581460
path->path.startup_cost = path->subpath->startup_cost;
14591461
path->path.total_cost = path->subpath->total_cost;
14601462

1463+
/*
1464+
* However, if there are no relevant restriction clauses and the
1465+
* pathtarget is trivial, then we expect that setrefs.c will optimize away
1466+
* the SubqueryScan plan node altogether, so we should just make its cost
1467+
* and rowcount equal to the input path's.
1468+
*
1469+
* Note: there are some edge cases where createplan.c will apply a
1470+
* different targetlist to the SubqueryScan node, thus falsifying our
1471+
* current estimate of whether the target is trivial, and making the cost
1472+
* estimate (though not the rowcount) wrong. It does not seem worth the
1473+
* extra complication to try to account for that exactly, especially since
1474+
* that behavior falsifies other cost estimates as well.
1475+
*/
1476+
if (qpquals == NIL && trivial_pathtarget)
1477+
return;
1478+
14611479
get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost);
14621480

14631481
startup_cost = qpqual_cost.startup;

src/backend/optimizer/plan/setrefs.c

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,9 +1636,10 @@ set_append_references(PlannerInfo *root,
16361636
/*
16371637
* See if it's safe to get rid of the Append entirely. For this to be
16381638
* safe, there must be only one child plan and that child plan's parallel
1639-
* awareness must match that of the Append's. The reason for the latter
1640-
* is that if the Append is parallel aware and the child is not, then the
1641-
* calling plan may execute the non-parallel aware child multiple times.
1639+
* awareness must match the Append's. The reason for the latter is that
1640+
* if the Append is parallel aware and the child is not, then the calling
1641+
* plan may execute the non-parallel aware child multiple times. (If you
1642+
* change these rules, update create_append_path to match.)
16421643
*/
16431644
if (list_length(aplan->appendplans) == 1)
16441645
{
@@ -1710,10 +1711,11 @@ set_mergeappend_references(PlannerInfo *root,
17101711
/*
17111712
* See if it's safe to get rid of the MergeAppend entirely. For this to
17121713
* be safe, there must be only one child plan and that child plan's
1713-
* parallel awareness must match that of the MergeAppend's. The reason
1714-
* for the latter is that if the MergeAppend is parallel aware and the
1715-
* child is not then the calling plan may execute the non-parallel aware
1716-
* child multiple times.
1714+
* parallel awareness must match the MergeAppend's. The reason for the
1715+
* latter is that if the MergeAppend is parallel aware and the child is
1716+
* not, then the calling plan may execute the non-parallel aware child
1717+
* multiple times. (If you change these rules, update
1718+
* create_merge_append_path to match.)
17171719
*/
17181720
if (list_length(mplan->mergeplans) == 1)
17191721
{

src/backend/optimizer/prep/prepunion.c

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ static List *generate_setop_tlist(List *colTypes, List *colCollations,
7878
Index varno,
7979
bool hack_constants,
8080
List *input_tlist,
81-
List *refnames_tlist);
81+
List *refnames_tlist,
82+
bool *trivial_tlist);
8283
static List *generate_append_tlist(List *colTypes, List *colCollations,
8384
bool flag,
8485
List *input_tlists,
@@ -226,6 +227,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
226227
Path *subpath;
227228
Path *path;
228229
List *tlist;
230+
bool trivial_tlist;
229231

230232
Assert(subquery != NULL);
231233

@@ -254,7 +256,8 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
254256
rtr->rtindex,
255257
true,
256258
subroot->processed_tlist,
257-
refnames_tlist);
259+
refnames_tlist,
260+
&trivial_tlist);
258261
rel->reltarget = create_pathtarget(root, tlist);
259262

260263
/* Return the fully-fledged tlist to caller, too */
@@ -291,6 +294,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
291294
* soon too, likely.)
292295
*/
293296
path = (Path *) create_subqueryscan_path(root, rel, subpath,
297+
trivial_tlist,
294298
NIL, NULL);
295299

296300
add_path(rel, path);
@@ -309,6 +313,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
309313
partial_subpath = linitial(final_rel->partial_pathlist);
310314
partial_path = (Path *)
311315
create_subqueryscan_path(root, rel, partial_subpath,
316+
trivial_tlist,
312317
NIL, NULL);
313318
add_partial_path(rel, partial_path);
314319
}
@@ -376,14 +381,16 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
376381
!tlist_same_collations(*pTargetList, colCollations, junkOK))
377382
{
378383
PathTarget *target;
384+
bool trivial_tlist;
379385
ListCell *lc;
380386

381387
*pTargetList = generate_setop_tlist(colTypes, colCollations,
382388
flag,
383389
0,
384390
false,
385391
*pTargetList,
386-
refnames_tlist);
392+
refnames_tlist,
393+
&trivial_tlist);
387394
target = create_pathtarget(root, *pTargetList);
388395

389396
/* Apply projection to each path */
@@ -1117,14 +1124,16 @@ choose_hashed_setop(PlannerInfo *root, List *groupClauses,
11171124
* hack_constants: true to copy up constants (see comments in code)
11181125
* input_tlist: targetlist of this node's input node
11191126
* refnames_tlist: targetlist to take column names from
1127+
* trivial_tlist: output parameter, set to true if targetlist is trivial
11201128
*/
11211129
static List *
11221130
generate_setop_tlist(List *colTypes, List *colCollations,
11231131
int flag,
11241132
Index varno,
11251133
bool hack_constants,
11261134
List *input_tlist,
1127-
List *refnames_tlist)
1135+
List *refnames_tlist,
1136+
bool *trivial_tlist)
11281137
{
11291138
List *tlist = NIL;
11301139
int resno = 1;
@@ -1135,6 +1144,8 @@ generate_setop_tlist(List *colTypes, List *colCollations,
11351144
TargetEntry *tle;
11361145
Node *expr;
11371146

1147+
*trivial_tlist = true; /* until proven differently */
1148+
11381149
forfour(ctlc, colTypes, cclc, colCollations,
11391150
itlc, input_tlist, rtlc, refnames_tlist)
11401151
{
@@ -1160,6 +1171,9 @@ generate_setop_tlist(List *colTypes, List *colCollations,
11601171
* this only at the first level of subquery-scan plans; we don't want
11611172
* phony constants appearing in the output tlists of upper-level
11621173
* nodes!
1174+
*
1175+
* Note that copying a constant doesn't in itself require us to mark
1176+
* the tlist nontrivial; see trivial_subqueryscan() in setrefs.c.
11631177
*/
11641178
if (hack_constants && inputtle->expr && IsA(inputtle->expr, Const))
11651179
expr = (Node *) inputtle->expr;
@@ -1185,6 +1199,7 @@ generate_setop_tlist(List *colTypes, List *colCollations,
11851199
expr,
11861200
colType,
11871201
"UNION/INTERSECT/EXCEPT");
1202+
*trivial_tlist = false; /* the coercion makes it not trivial */
11881203
}
11891204

11901205
/*
@@ -1199,9 +1214,12 @@ generate_setop_tlist(List *colTypes, List *colCollations,
11991214
* will reach the executor without any further processing.
12001215
*/
12011216
if (exprCollation(expr) != colColl)
1217+
{
12021218
expr = applyRelabelType(expr,
12031219
exprType(expr), exprTypmod(expr), colColl,
12041220
COERCE_IMPLICIT_CAST, -1, false);
1221+
*trivial_tlist = false; /* the relabel makes it not trivial */
1222+
}
12051223

12061224
tle = makeTargetEntry((Expr *) expr,
12071225
(AttrNumber) resno++,
@@ -1234,6 +1252,7 @@ generate_setop_tlist(List *colTypes, List *colCollations,
12341252
pstrdup("flag"),
12351253
true);
12361254
tlist = lappend(tlist, tle);
1255+
*trivial_tlist = false; /* the extra entry makes it not trivial */
12371256
}
12381257

12391258
return tlist;

src/backend/optimizer/util/pathnode.c

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,19 +1326,28 @@ create_append_path(PlannerInfo *root,
13261326
Assert(!parallel_aware || pathnode->path.parallel_safe);
13271327

13281328
/*
1329-
* If there's exactly one child path, the Append is a no-op and will be
1330-
* discarded later (in setrefs.c); therefore, we can inherit the child's
1331-
* size and cost, as well as its pathkeys if any (overriding whatever the
1332-
* caller might've said). Otherwise, we must do the normal costsize
1329+
* If there's exactly one child path then the output of the Append is
1330+
* necessarily ordered the same as the child's, so we can inherit the
1331+
* child's pathkeys if any, overriding whatever the caller might've said.
1332+
* Furthermore, if the child's parallel awareness matches the Append's,
1333+
* then the Append is a no-op and will be discarded later (in setrefs.c).
1334+
* Then we can inherit the child's size and cost too, effectively charging
1335+
* zero for the Append. Otherwise, we must do the normal costsize
13331336
* calculation.
13341337
*/
13351338
if (list_length(pathnode->subpaths) == 1)
13361339
{
13371340
Path *child = (Path *) linitial(pathnode->subpaths);
13381341

1339-
pathnode->path.rows = child->rows;
1340-
pathnode->path.startup_cost = child->startup_cost;
1341-
pathnode->path.total_cost = child->total_cost;
1342+
if (child->parallel_aware == parallel_aware)
1343+
{
1344+
pathnode->path.rows = child->rows;
1345+
pathnode->path.startup_cost = child->startup_cost;
1346+
pathnode->path.total_cost = child->total_cost;
1347+
}
1348+
else
1349+
cost_append(pathnode, root);
1350+
/* Must do this last, else cost_append complains */
13421351
pathnode->path.pathkeys = child->pathkeys;
13431352
}
13441353
else
@@ -1476,10 +1485,13 @@ create_merge_append_path(PlannerInfo *root,
14761485

14771486
/*
14781487
* Now we can compute total costs of the MergeAppend. If there's exactly
1479-
* one child path, the MergeAppend is a no-op and will be discarded later
1480-
* (in setrefs.c); otherwise we do the normal cost calculation.
1488+
* one child path and its parallel awareness matches that of the
1489+
* MergeAppend, then the MergeAppend is a no-op and will be discarded
1490+
* later (in setrefs.c); otherwise we do the normal cost calculation.
14811491
*/
1482-
if (list_length(subpaths) == 1)
1492+
if (list_length(subpaths) == 1 &&
1493+
((Path *) linitial(subpaths))->parallel_aware ==
1494+
pathnode->path.parallel_aware)
14831495
{
14841496
pathnode->path.startup_cost = input_startup_cost;
14851497
pathnode->path.total_cost = input_total_cost;
@@ -1986,9 +1998,15 @@ create_gather_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
19861998
* create_subqueryscan_path
19871999
* Creates a path corresponding to a scan of a subquery,
19882000
* returning the pathnode.
2001+
*
2002+
* Caller must pass trivial_pathtarget = true if it believes rel->reltarget to
2003+
* be trivial, ie just a fetch of all the subquery output columns in order.
2004+
* While we could determine that here, the caller can usually do it more
2005+
* efficiently (or at least amortize it over multiple calls).
19892006
*/
19902007
SubqueryScanPath *
19912008
create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
2009+
bool trivial_pathtarget,
19922010
List *pathkeys, Relids required_outer)
19932011
{
19942012
SubqueryScanPath *pathnode = makeNode(SubqueryScanPath);
@@ -2005,7 +2023,8 @@ create_subqueryscan_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
20052023
pathnode->path.pathkeys = pathkeys;
20062024
pathnode->subpath = subpath;
20072025

2008-
cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info);
2026+
cost_subqueryscan(pathnode, root, rel, pathnode->path.param_info,
2027+
trivial_pathtarget);
20092028

20102029
return pathnode;
20112030
}
@@ -3901,10 +3920,23 @@ reparameterize_path(PlannerInfo *root, Path *path,
39013920
case T_SubqueryScan:
39023921
{
39033922
SubqueryScanPath *spath = (SubqueryScanPath *) path;
3923+
Path *subpath = spath->subpath;
3924+
bool trivial_pathtarget;
3925+
3926+
/*
3927+
* If existing node has zero extra cost, we must have decided
3928+
* its target is trivial. (The converse is not true, because
3929+
* it might have a trivial target but quals to enforce; but in
3930+
* that case the new node will too, so it doesn't matter
3931+
* whether we get the right answer here.)
3932+
*/
3933+
trivial_pathtarget =
3934+
(subpath->total_cost == spath->path.total_cost);
39043935

39053936
return (Path *) create_subqueryscan_path(root,
39063937
rel,
3907-
spath->subpath,
3938+
subpath,
3939+
trivial_pathtarget,
39083940
spath->path.pathkeys,
39093941
required_outer);
39103942
}

src/include/optimizer/cost.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ extern void cost_tidrangescan(Path *path, PlannerInfo *root,
9191
RelOptInfo *baserel, List *tidrangequals,
9292
ParamPathInfo *param_info);
9393
extern void cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
94-
RelOptInfo *baserel, ParamPathInfo *param_info);
94+
RelOptInfo *baserel, ParamPathInfo *param_info,
95+
bool trivial_pathtarget);
9596
extern void cost_functionscan(Path *path, PlannerInfo *root,
9697
RelOptInfo *baserel, ParamPathInfo *param_info);
9798
extern void cost_valuesscan(Path *path, PlannerInfo *root,

src/include/optimizer/pathnode.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,11 @@ extern GatherMergePath *create_gather_merge_path(PlannerInfo *root,
103103
Relids required_outer,
104104
double *rows);
105105
extern SubqueryScanPath *create_subqueryscan_path(PlannerInfo *root,
106-
RelOptInfo *rel, Path *subpath,
107-
List *pathkeys, Relids required_outer);
106+
RelOptInfo *rel,
107+
Path *subpath,
108+
bool trivial_pathtarget,
109+
List *pathkeys,
110+
Relids required_outer);
108111
extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel,
109112
List *pathkeys, Relids required_outer);
110113
extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel,

0 commit comments

Comments
 (0)