Estimate cost of elided SubqueryScan, Append, MergeAppend nodes better.
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 19 Jul 2022 15:18:19 +0000 (11:18 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 19 Jul 2022 15:18:19 +0000 (11:18 -0400)
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

src/backend/optimizer/path/allpaths.c
src/backend/optimizer/path/costsize.c
src/backend/optimizer/plan/setrefs.c
src/backend/optimizer/prep/prepunion.c
src/backend/optimizer/util/pathnode.c
src/include/optimizer/cost.h
src/include/optimizer/pathnode.h
src/test/regress/expected/create_view.out
src/test/regress/expected/join.out

index 358bb2aed6f48f7d044bbe2622709c4c2f7179d9..028d9e168080dc0fc462e52394862abee83f7b2b 100644 (file)
@@ -2451,6 +2451,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
 {
    Query      *parse = root->parse;
    Query      *subquery = rte->subquery;
+   bool        trivial_pathtarget;
    Relids      required_outer;
    pushdown_safety_info safetyInfo;
    double      tuple_fraction;
@@ -2613,6 +2614,36 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
     */
    set_subquery_size_estimates(root, rel);
 
+   /*
+    * Also detect whether the reltarget is trivial, so that we can pass that
+    * info to cost_subqueryscan (rather than re-deriving it multiple times).
+    * It's trivial if it fetches all the subplan output columns in order.
+    */
+   if (list_length(rel->reltarget->exprs) != list_length(subquery->targetList))
+       trivial_pathtarget = false;
+   else
+   {
+       trivial_pathtarget = true;
+       foreach(lc, rel->reltarget->exprs)
+       {
+           Node       *node = (Node *) lfirst(lc);
+           Var        *var;
+
+           if (!IsA(node, Var))
+           {
+               trivial_pathtarget = false;
+               break;
+           }
+           var = (Var *) node;
+           if (var->varno != rti ||
+               var->varattno != foreach_current_index(lc) + 1)
+           {
+               trivial_pathtarget = false;
+               break;
+           }
+       }
+   }
+
    /*
     * For each Path that subquery_planner produced, make a SubqueryScanPath
     * in the outer query.
@@ -2631,6 +2662,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
        /* Generate outer path using this subpath */
        add_path(rel, (Path *)
                 create_subqueryscan_path(root, rel, subpath,
+                                         trivial_pathtarget,
                                          pathkeys, required_outer));
    }
 
@@ -2656,6 +2688,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
            /* Generate outer path using this subpath */
            add_partial_path(rel, (Path *)
                             create_subqueryscan_path(root, rel, subpath,
+                                                     trivial_pathtarget,
                                                      pathkeys,
                                                      required_outer));
        }
index 5e5732f6e1207e96489777d19b4d08d36e557961..fb28e6411aa7a4d935f0f6d1821a0ef2945a66cf 100644 (file)
@@ -1415,10 +1415,12 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
  *
  * 'baserel' is the relation to be scanned
  * 'param_info' is the ParamPathInfo if this is a parameterized path, else NULL
+ * 'trivial_pathtarget' is true if the pathtarget is believed to be trivial.
  */
 void
 cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
-                 RelOptInfo *baserel, ParamPathInfo *param_info)
+                 RelOptInfo *baserel, ParamPathInfo *param_info,
+                 bool trivial_pathtarget)
 {
    Cost        startup_cost;
    Cost        run_cost;
@@ -1458,6 +1460,22 @@ cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
    path->path.startup_cost = path->subpath->startup_cost;
    path->path.total_cost = path->subpath->total_cost;
 
+   /*
+    * However, if there are no relevant restriction clauses and the
+    * pathtarget is trivial, then we expect that setrefs.c will optimize away
+    * the SubqueryScan plan node altogether, so we should just make its cost
+    * and rowcount equal to the input path's.
+    *
+    * Note: there are some edge cases where createplan.c will apply a
+    * different targetlist to the SubqueryScan node, thus falsifying our
+    * current estimate of whether the target is trivial, and making the cost
+    * estimate (though not the rowcount) wrong.  It does not seem worth the
+    * extra complication to try to account for that exactly, especially since
+    * that behavior falsifies other cost estimates as well.
+    */
+   if (qpquals == NIL && trivial_pathtarget)
+       return;
+
    get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost);
 
    startup_cost = qpqual_cost.startup;
index 707c1016c25a076477608379c0c24495cdaab5b5..1cb0abdbc1f9e7ce975292eebb480a20021f89da 100644 (file)
@@ -1636,9 +1636,10 @@ set_append_references(PlannerInfo *root,
    /*
     * See if it's safe to get rid of the Append entirely.  For this to be
     * safe, there must be only one child plan and that child plan's parallel
-    * awareness must match that of the Append's.  The reason for the latter
-    * is that if the Append is parallel aware and the child is not, then the
-    * calling plan may execute the non-parallel aware child multiple times.
+    * awareness must match the Append's.  The reason for the latter is that
+    * if the Append is parallel aware and the child is not, then the calling
+    * plan may execute the non-parallel aware child multiple times.  (If you
+    * change these rules, update create_append_path to match.)
     */
    if (list_length(aplan->appendplans) == 1)
    {
@@ -1710,10 +1711,11 @@ set_mergeappend_references(PlannerInfo *root,
    /*
     * See if it's safe to get rid of the MergeAppend entirely.  For this to
     * be safe, there must be only one child plan and that child plan's
-    * parallel awareness must match that of the MergeAppend's.  The reason
-    * for the latter is that if the MergeAppend is parallel aware and the
-    * child is not then the calling plan may execute the non-parallel aware
-    * child multiple times.
+    * parallel awareness must match the MergeAppend's.  The reason for the
+    * latter is that if the MergeAppend is parallel aware and the child is
+    * not, then the calling plan may execute the non-parallel aware child
+    * multiple times.  (If you change these rules, update
+    * create_merge_append_path to match.)
     */
    if (list_length(mplan->mergeplans) == 1)
    {
index f004fad1d920066f702d9d3f5d62b05f38de9ee2..2214920dea4d9f2d99f174081072740a2bfd7b23 100644 (file)
@@ -78,7 +78,8 @@ static List *generate_setop_tlist(List *colTypes, List *colCollations,
                                  Index varno,
                                  bool hack_constants,
                                  List *input_tlist,
-                                 List *refnames_tlist);
+                                 List *refnames_tlist,
+                                 bool *trivial_tlist);
 static List *generate_append_tlist(List *colTypes, List *colCollations,
                                   bool flag,
                                   List *input_tlists,
@@ -226,6 +227,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
        Path       *subpath;
        Path       *path;
        List       *tlist;
+       bool        trivial_tlist;
 
        Assert(subquery != NULL);
 
@@ -254,7 +256,8 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
                                     rtr->rtindex,
                                     true,
                                     subroot->processed_tlist,
-                                    refnames_tlist);
+                                    refnames_tlist,
+                                    &trivial_tlist);
        rel->reltarget = create_pathtarget(root, tlist);
 
        /* Return the fully-fledged tlist to caller, too */
@@ -291,6 +294,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
         * soon too, likely.)
         */
        path = (Path *) create_subqueryscan_path(root, rel, subpath,
+                                                trivial_tlist,
                                                 NIL, NULL);
 
        add_path(rel, path);
@@ -309,6 +313,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
            partial_subpath = linitial(final_rel->partial_pathlist);
            partial_path = (Path *)
                create_subqueryscan_path(root, rel, partial_subpath,
+                                        trivial_tlist,
                                         NIL, NULL);
            add_partial_path(rel, partial_path);
        }
@@ -376,6 +381,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
            !tlist_same_collations(*pTargetList, colCollations, junkOK))
        {
            PathTarget *target;
+           bool        trivial_tlist;
            ListCell   *lc;
 
            *pTargetList = generate_setop_tlist(colTypes, colCollations,
@@ -383,7 +389,8 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
                                                0,
                                                false,
                                                *pTargetList,
-                                               refnames_tlist);
+                                               refnames_tlist,
+                                               &trivial_tlist);
            target = create_pathtarget(root, *pTargetList);
 
            /* Apply projection to each path */
@@ -1117,6 +1124,7 @@ choose_hashed_setop(PlannerInfo *root, List *groupClauses,
  * hack_constants: true to copy up constants (see comments in code)
  * input_tlist: targetlist of this node's input node
  * refnames_tlist: targetlist to take column names from
+ * trivial_tlist: output parameter, set to true if targetlist is trivial
  */
 static List *
 generate_setop_tlist(List *colTypes, List *colCollations,
@@ -1124,7 +1132,8 @@ generate_setop_tlist(List *colTypes, List *colCollations,
                     Index varno,
                     bool hack_constants,
                     List *input_tlist,
-                    List *refnames_tlist)
+                    List *refnames_tlist,
+                    bool *trivial_tlist)
 {
    List       *tlist = NIL;
    int         resno = 1;
@@ -1135,6 +1144,8 @@ generate_setop_tlist(List *colTypes, List *colCollations,
    TargetEntry *tle;
    Node       *expr;
 
+   *trivial_tlist = true;      /* until proven differently */
+
    forfour(ctlc, colTypes, cclc, colCollations,
            itlc, input_tlist, rtlc, refnames_tlist)
    {
@@ -1160,6 +1171,9 @@ generate_setop_tlist(List *colTypes, List *colCollations,
         * this only at the first level of subquery-scan plans; we don't want
         * phony constants appearing in the output tlists of upper-level
         * nodes!
+        *
+        * Note that copying a constant doesn't in itself require us to mark
+        * the tlist nontrivial; see trivial_subqueryscan() in setrefs.c.
         */
        if (hack_constants && inputtle->expr && IsA(inputtle->expr, Const))
            expr = (Node *) inputtle->expr;
@@ -1185,6 +1199,7 @@ generate_setop_tlist(List *colTypes, List *colCollations,
                                         expr,
                                         colType,
                                         "UNION/INTERSECT/EXCEPT");
+           *trivial_tlist = false; /* the coercion makes it not trivial */
        }
 
        /*
@@ -1199,9 +1214,12 @@ generate_setop_tlist(List *colTypes, List *colCollations,
         * will reach the executor without any further processing.
         */
        if (exprCollation(expr) != colColl)
+       {
            expr = applyRelabelType(expr,
                                    exprType(expr), exprTypmod(expr), colColl,
                                    COERCE_IMPLICIT_CAST, -1, false);
+           *trivial_tlist = false; /* the relabel makes it not trivial */
+       }
 
        tle = makeTargetEntry((Expr *) expr,
                              (AttrNumber) resno++,
@@ -1234,6 +1252,7 @@ generate_setop_tlist(List *colTypes, List *colCollations,
                              pstrdup("flag"),
                              true);
        tlist = lappend(tlist, tle);
+       *trivial_tlist = false; /* the extra entry makes it not trivial */
    }
 
    return tlist;
index 483c4f41373d832fa8b028c6b11f6189a8364ae8..dd64b460865d8e45a3407fb60eb4778a7213fcc2 100644 (file)
@@ -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);
            }
index dc7fc1741141beb74368f3670d2104231e0af26f..9e91da7114f620c17148029de5e5b3b690823771 100644 (file)
@@ -91,7 +91,8 @@ extern void cost_tidrangescan(Path *path, PlannerInfo *root,
                              RelOptInfo *baserel, List *tidrangequals,
                              ParamPathInfo *param_info);
 extern void cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
-                             RelOptInfo *baserel, ParamPathInfo *param_info);
+                             RelOptInfo *baserel, ParamPathInfo *param_info,
+                             bool trivial_pathtarget);
 extern void cost_functionscan(Path *path, PlannerInfo *root,
                              RelOptInfo *baserel, ParamPathInfo *param_info);
 extern void cost_valuesscan(Path *path, PlannerInfo *root,
index 635cc0a0a66b5759a059bcacf54523ef04ced24d..050f00e79a4fbe2b4e3f4d236b9394d1041ad116 100644 (file)
@@ -103,8 +103,11 @@ extern GatherMergePath *create_gather_merge_path(PlannerInfo *root,
                                                 Relids required_outer,
                                                 double *rows);
 extern SubqueryScanPath *create_subqueryscan_path(PlannerInfo *root,
-                                                 RelOptInfo *rel, Path *subpath,
-                                                 List *pathkeys, Relids required_outer);
+                                                 RelOptInfo *rel,
+                                                 Path *subpath,
+                                                 bool trivial_pathtarget,
+                                                 List *pathkeys,
+                                                 Relids required_outer);
 extern Path *create_functionscan_path(PlannerInfo *root, RelOptInfo *rel,
                                      List *pathkeys, Relids required_outer);
 extern Path *create_valuesscan_path(PlannerInfo *root, RelOptInfo *rel,
index 32385bbb0ef2d62f6d8041f4f291e87ca4fa62f9..9d4f9011a8febedef5c7dab9801469865127d7ac 100644 (file)
@@ -1976,18 +1976,18 @@ select * from tt24v;
 ------------------------------------------------------------------------------------------
  Hash Join
    Output: (cte.r).column2, ((ROW("*VALUES*".column1, "*VALUES*".column2))).column2
-   Hash Cond: (((ROW("*VALUES*".column1, "*VALUES*".column2))).column1 = (cte.r).column1)
+   Hash Cond: ((cte.r).column1 = ((ROW("*VALUES*".column1, "*VALUES*".column2))).column1)
    CTE cte
      ->  Values Scan on "*VALUES*_1"
            Output: ROW("*VALUES*_1".column1, "*VALUES*_1".column2)
-   ->  Limit
-         Output: (ROW("*VALUES*".column1, "*VALUES*".column2))
-         ->  Values Scan on "*VALUES*"
-               Output: ROW("*VALUES*".column1, "*VALUES*".column2)
-   ->  Hash
+   ->  CTE Scan on cte
          Output: cte.r
-         ->  CTE Scan on cte
-               Output: cte.r
+   ->  Hash
+         Output: (ROW("*VALUES*".column1, "*VALUES*".column2))
+         ->  Limit
+               Output: (ROW("*VALUES*".column1, "*VALUES*".column2))
+               ->  Values Scan on "*VALUES*"
+                     Output: ROW("*VALUES*".column1, "*VALUES*".column2)
 (14 rows)
 
 explain (verbose, costs off)
index 1f0df6b7d941801772085dc865c33b415122d7fc..e1d9d971d650a2cbb8e9fc9992f673e36c7da7d5 100644 (file)
@@ -5207,11 +5207,11 @@ explain (costs off)
    Sort Key: a.q1, a.q2, x.q1, x.q2, (a.q1)
    ->  Nested Loop
          ->  Seq Scan on int8_tbl a
-         ->  Hash Right Join
-               Hash Cond: ((a.q1) = x.q2)
-               ->  Seq Scan on int4_tbl y
+         ->  Hash Left Join
+               Hash Cond: (x.q2 = (a.q1))
+               ->  Seq Scan on int8_tbl x
                ->  Hash
-                     ->  Seq Scan on int8_tbl x
+                     ->  Seq Scan on int4_tbl y
 (9 rows)
 
 select * from int8_tbl a,