Re: Optimizer questions

Поиск
Список
Период
Сортировка
От Tom Lane
Тема Re: Optimizer questions
Дата
Msg-id 28389.1457660734@sss.pgh.pa.us
обсуждение исходный текст
Ответ на Re: Optimizer questions  (Tom Lane <tgl@sss.pgh.pa.us>)
Список pgsql-hackers
I wrote:
> As far as that goes, it seems to me after thinking about it that
> non-sort-column tlist items containing SRFs should always be postponed,
> too.  Performing a SRF before sorting bloats the sort data vertically,
> rather than horizontally, but it's still bloat.  (Although as against
> that, when you have ORDER BY + LIMIT, postponing SRFs loses the ability
> to use a bounded sort.)  The killer point though is that unless the sort
> is stable, it might cause surprising changes in the order of the SRF
> output values.  Our sorts aren't stable; here's an example in HEAD:

> # select q1, generate_series(1,9) from int8_tbl order by q1 limit 7;
>  q1  | generate_series
> -----+-----------------
>  123 |               2
>  123 |               3
>  123 |               4
>  123 |               5
>  123 |               6
>  123 |               7
>  123 |               1
> (7 rows)

> I think that's pretty surprising, and if we have an opportunity to
> provide more intuitive semantics here, we should do it.

Here's an updated patch (requires current HEAD) that takes care of
window functions correctly and also does something reasonable with
set-returning functions:

# explain verbose select q1, generate_series(1,9) from int8_tbl order by q1 limit 7;
                                   QUERY PLAN
---------------------------------------------------------------------------------
 Limit  (cost=1.11..1.14 rows=7 width=12)
   Output: q1, (generate_series(1, 9))
   ->  Result  (cost=1.11..26.16 rows=5000 width=12)
         Output: q1, generate_series(1, 9)
         ->  Sort  (cost=1.11..1.12 rows=5 width=8)
               Output: q1
               Sort Key: int8_tbl.q1
               ->  Seq Scan on public.int8_tbl  (cost=0.00..1.05 rows=5 width=8)
                     Output: q1
(9 rows)

# select q1, generate_series(1,9) from int8_tbl order by q1 limit 7;
 q1  | generate_series
-----+-----------------
 123 |               1
 123 |               2
 123 |               3
 123 |               4
 123 |               5
 123 |               6
 123 |               7
(7 rows)

I added some user-facing documentation too.  I think this is committable,
though maybe we should add a regression test case or two.

            regards, tom lane

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 44810f4..0520f2c 100644
*** a/doc/src/sgml/ref/select.sgml
--- b/doc/src/sgml/ref/select.sgml
*************** UNBOUNDED FOLLOWING
*** 993,998 ****
--- 993,1028 ----
      cases it is not possible to specify new names with <literal>AS</>;
      the output column names will be the same as the table columns' names.
     </para>
+
+    <para>
+     According to the SQL standard, the expressions in the output list should
+     be computed before applying <literal>DISTINCT</literal>, <literal>ORDER
+     BY</literal>, or <literal>LIMIT</literal>.  This is obviously necessary
+     when using <literal>DISTINCT</literal>, since otherwise it's not clear
+     what values are being made distinct.  However, in many cases it is
+     convenient if output expressions are computed after <literal>ORDER
+     BY</literal> and <literal>LIMIT</literal>; particularly if the output list
+     contains any volatile or expensive functions.  With that behavior, the
+     order of function evaluations is more intuitive and there will not be
+     evaluations corresponding to rows that never appear in the output.
+     <productname>PostgreSQL</> will effectively evaluate output expressions
+     after sorting and limiting, so long as those expressions are not
+     referenced in <literal>DISTINCT</literal>, <literal>ORDER BY</literal>
+     or <literal>GROUP BY</literal>.  (As a counterexample, <literal>SELECT
+     f(x) FROM tab ORDER BY 1</> clearly must evaluate <function>f(x)</>
+     before sorting.)  Output expressions that contain set-returning functions
+     are effectively evaluated after sorting and before limiting, so
+     that <literal>LIMIT</literal> will act to cut off the output from a
+     set-returning function.
+    </para>
+
+    <note>
+     <para>
+      <productname>PostgreSQL</> versions before 9.6 did not provide any
+      guarantees about the timing of evaluation of output expressions versus
+      sorting and limiting; it depended on the form of the chosen query plan.
+     </para>
+    </note>
    </refsect2>

    <refsect2 id="sql-distinct">
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index a2cd6de..51f4ee4 100644
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** static RelOptInfo *create_distinct_paths
*** 127,132 ****
--- 127,133 ----
                        RelOptInfo *input_rel);
  static RelOptInfo *create_ordered_paths(PlannerInfo *root,
                       RelOptInfo *input_rel,
+                      PathTarget *target,
                       double limit_tuples);
  static PathTarget *make_group_input_target(PlannerInfo *root, List *tlist);
  static List *postprocess_setop_tlist(List *new_tlist, List *orig_tlist);
*************** static PathTarget *make_window_input_tar
*** 135,140 ****
--- 136,144 ----
                           List *tlist, List *activeWindows);
  static List *make_pathkeys_for_window(PlannerInfo *root, WindowClause *wc,
                           List *tlist);
+ static PathTarget *make_sort_input_target(PlannerInfo *root,
+                        PathTarget *final_target,
+                        bool *have_postponed_srfs);


  /*****************************************************************************
*************** grouping_planner(PlannerInfo *root, bool
*** 1379,1384 ****
--- 1383,1391 ----
      int64        offset_est = 0;
      int64        count_est = 0;
      double        limit_tuples = -1.0;
+     bool        have_postponed_srfs = false;
+     double        tlist_rows;
+     PathTarget *final_target;
      RelOptInfo *current_rel;
      RelOptInfo *final_rel;
      ListCell   *lc;
*************** grouping_planner(PlannerInfo *root, bool
*** 1438,1443 ****
--- 1445,1453 ----
          /* Save aside the final decorated tlist */
          root->processed_tlist = tlist;

+         /* Also extract the PathTarget form of the setop result tlist */
+         final_target = current_rel->cheapest_total_path->pathtarget;
+
          /*
           * Can't handle FOR [KEY] UPDATE/SHARE here (parser should have
           * checked already, but let's make sure).
*************** grouping_planner(PlannerInfo *root, bool
*** 1462,1472 ****
      else
      {
          /* No set operations, do regular planning */
!         PathTarget *final_target;
          PathTarget *grouping_target;
          PathTarget *scanjoin_target;
          bool        have_grouping;
-         double        tlist_rows;
          WindowFuncLists *wflists = NULL;
          List       *activeWindows = NIL;
          List       *rollup_lists = NIL;
--- 1472,1481 ----
      else
      {
          /* No set operations, do regular planning */
!         PathTarget *sort_input_target;
          PathTarget *grouping_target;
          PathTarget *scanjoin_target;
          bool        have_grouping;
          WindowFuncLists *wflists = NULL;
          List       *activeWindows = NIL;
          List       *rollup_lists = NIL;
*************** grouping_planner(PlannerInfo *root, bool
*** 1620,1626 ****
           * Figure out whether there's a hard limit on the number of rows that
           * query_planner's result subplan needs to return.  Even if we know a
           * hard limit overall, it doesn't apply if the query has any
!          * grouping/aggregation operations.
           */
          if (parse->groupClause ||
              parse->groupingSets ||
--- 1629,1638 ----
           * Figure out whether there's a hard limit on the number of rows that
           * query_planner's result subplan needs to return.  Even if we know a
           * hard limit overall, it doesn't apply if the query has any
!          * grouping/aggregation operations.  (XXX it also doesn't apply if the
!          * tlist contains any SRFs; but checking for that here seems more
!          * costly than it's worth, since root->limit_tuples is only used for
!          * cost estimates, and only in a small number of cases.)
           */
          if (parse->groupClause ||
              parse->groupingSets ||
*************** grouping_planner(PlannerInfo *root, bool
*** 1658,1679 ****
          final_target = create_pathtarget(root, tlist);

          /*
           * If we have window functions to deal with, the output from any
           * grouping step needs to be what the window functions want;
!          * otherwise, it should just be final_target.
           */
          if (activeWindows)
              grouping_target = make_window_input_target(root,
                                                         tlist,
                                                         activeWindows);
          else
!             grouping_target = final_target;

          /*
           * If we have grouping or aggregation to do, the topmost scan/join
!          * plan node must emit what the grouping step wants; otherwise, if
!          * there's window functions, it must emit what the window functions
!          * want; otherwise, it should emit final_target.
           */
          have_grouping = (parse->groupClause || parse->groupingSets ||
                           parse->hasAggs || root->hasHavingQual);
--- 1670,1701 ----
          final_target = create_pathtarget(root, tlist);

          /*
+          * If ORDER BY was given, consider whether we should use a post-sort
+          * projection, and compute the adjusted target for preceding steps if
+          * so.
+          */
+         if (parse->sortClause)
+             sort_input_target = make_sort_input_target(root, final_target,
+                                                        &have_postponed_srfs);
+         else
+             sort_input_target = final_target;
+
+         /*
           * If we have window functions to deal with, the output from any
           * grouping step needs to be what the window functions want;
!          * otherwise, it should be sort_input_target.
           */
          if (activeWindows)
              grouping_target = make_window_input_target(root,
                                                         tlist,
                                                         activeWindows);
          else
!             grouping_target = sort_input_target;

          /*
           * If we have grouping or aggregation to do, the topmost scan/join
!          * plan node must emit what the grouping step wants; otherwise, it
!          * should emit grouping_target.
           */
          have_grouping = (parse->groupClause || parse->groupingSets ||
                           parse->hasAggs || root->hasHavingQual);
*************** grouping_planner(PlannerInfo *root, bool
*** 1734,1779 ****
              current_rel = create_window_paths(root,
                                                current_rel,
                                                grouping_target,
!                                               final_target,
                                                tlist,
                                                wflists,
                                                activeWindows);
          }

          /*
-          * If there are set-returning functions in the tlist, scale up the
-          * assumed output rowcounts of all surviving Paths to account for
-          * that.  This is a bit of a kluge, but it's not clear how to account
-          * for it in a more principled way.  We definitely don't want to apply
-          * the multiplier more than once, which would happen if we tried to
-          * fold it into PathTarget accounting.  And the expansion does happen
-          * before any explicit DISTINCT or ORDER BY processing is done.
-          */
-         tlist_rows = tlist_returns_set_rows(tlist);
-         if (tlist_rows > 1)
-         {
-             foreach(lc, current_rel->pathlist)
-             {
-                 Path       *path = (Path *) lfirst(lc);
-
-                 /*
-                  * We assume that execution costs of the tlist as such were
-                  * already accounted for.  However, it still seems appropriate
-                  * to charge something more for the executor's general costs
-                  * of processing the added tuples.  The cost is probably less
-                  * than cpu_tuple_cost, though, so we arbitrarily use half of
-                  * that.
-                  */
-                 path->total_cost += path->rows * (tlist_rows - 1) *
-                     cpu_tuple_cost / 2;
-
-                 path->rows *= tlist_rows;
-             }
-
-             /* There seems no need for a fresh set_cheapest comparison. */
-         }
-
-         /*
           * If there is a DISTINCT clause, consider ways to implement that. We
           * build a new upperrel representing the output of this phase.
           */
--- 1756,1768 ----
              current_rel = create_window_paths(root,
                                                current_rel,
                                                grouping_target,
!                                               sort_input_target,
                                                tlist,
                                                wflists,
                                                activeWindows);
          }

          /*
           * If there is a DISTINCT clause, consider ways to implement that. We
           * build a new upperrel representing the output of this phase.
           */
*************** grouping_planner(PlannerInfo *root, bool
*** 1787,1803 ****

      /*
       * If ORDER BY was given, consider ways to implement that, and generate a
!      * new upperrel containing only paths that emit the correct ordering.  We
!      * can apply the original limit_tuples limit in sorting now.
       */
      if (parse->sortClause)
      {
          current_rel = create_ordered_paths(root,
                                             current_rel,
                                             limit_tuples);
      }

      /*
       * Now we are prepared to build the final-output upperrel.  Insert all
       * surviving paths, with LockRows, Limit, and/or ModifyTable steps added
       * if needed.
--- 1776,1826 ----

      /*
       * If ORDER BY was given, consider ways to implement that, and generate a
!      * new upperrel containing only paths that emit the correct ordering and
!      * project the correct final_target.  We can apply the original
!      * limit_tuples limit in sort costing here, but only if there are no
!      * postponed SRFs.
       */
      if (parse->sortClause)
      {
          current_rel = create_ordered_paths(root,
                                             current_rel,
+                                            final_target,
+                                            have_postponed_srfs ? -1.0 :
                                             limit_tuples);
      }

      /*
+      * If there are set-returning functions in the tlist, scale up the output
+      * rowcounts of all surviving Paths to account for that.  Note that if any
+      * SRFs appear in sorting or grouping columns, we'll have underestimated
+      * the numbers of rows passing through earlier steps; but that's such a
+      * weird usage that it doesn't seem worth greatly complicating matters to
+      * account for it.
+      */
+     tlist_rows = tlist_returns_set_rows(tlist);
+     if (tlist_rows > 1)
+     {
+         foreach(lc, current_rel->pathlist)
+         {
+             Path       *path = (Path *) lfirst(lc);
+
+             /*
+              * We assume that execution costs of the tlist as such were
+              * already accounted for.  However, it still seems appropriate to
+              * charge something more for the executor's general costs of
+              * processing the added tuples.  The cost is probably less than
+              * cpu_tuple_cost, though, so we arbitrarily use half of that.
+              */
+             path->total_cost += path->rows * (tlist_rows - 1) *
+                 cpu_tuple_cost / 2;
+
+             path->rows *= tlist_rows;
+         }
+         /* No need to run set_cheapest; we're keeping all paths anyway. */
+     }
+
+     /*
       * Now we are prepared to build the final-output upperrel.  Insert all
       * surviving paths, with LockRows, Limit, and/or ModifyTable steps added
       * if needed.
*************** create_distinct_paths(PlannerInfo *root,
*** 3705,3716 ****
--- 3728,3741 ----
   * cheapest-total existing path.
   *
   * input_rel: contains the source-data Paths
+  * target: the output tlist the result Paths must emit
   * limit_tuples: estimated bound on the number of output tuples,
   *        or -1 if no LIMIT or couldn't estimate
   */
  static RelOptInfo *
  create_ordered_paths(PlannerInfo *root,
                       RelOptInfo *input_rel,
+                      PathTarget *target,
                       double limit_tuples)
  {
      Path       *cheapest_input_path = input_rel->cheapest_total_path;
*************** create_ordered_paths(PlannerInfo *root,
*** 3738,3743 ****
--- 3763,3774 ----
                                                   root->sort_pathkeys,
                                                   limit_tuples);
              }
+
+             /* Add projection step if needed */
+             if (path->pathtarget != target)
+                 path = apply_projection_to_path(root, ordered_rel,
+                                                 path, target);
+
              add_path(ordered_rel, path);
          }
      }
*************** make_pathkeys_for_window(PlannerInfo *ro
*** 4144,4149 ****
--- 4175,4383 ----
  }

  /*
+  * make_sort_input_target
+  *      Generate appropriate PathTarget for initial input to Sort step.
+  *
+  * If the query has ORDER BY, this function chooses the target to be computed
+  * by the node just below the Sort (and DISTINCT, if any, since Unique can't
+  * project) steps.  This might or might not be identical to the query's final
+  * output target.
+  *
+  * The main argument for keeping the sort-input tlist the same as the final
+  * is that we avoid a separate projection node (which will be needed if
+  * they're different, because Sort can't project).  However, there are also
+  * advantages to postponing tlist evaluation till after the Sort: it ensures
+  * a consistent order of evaluation for any volatile functions in the tlist,
+  * and if there's also a LIMIT, we can stop the query without ever computing
+  * tlist functions for later rows, which is beneficial for both volatile and
+  * expensive functions.
+  *
+  * Our current policy is to postpone volatile expressions till after the sort
+  * unconditionally (assuming that that's possible, ie they are in plain tlist
+  * columns and not ORDER BY/GROUP BY/DISTINCT columns).  We also postpone
+  * set-returning expressions unconditionally (if possible), because running
+  * them beforehand would bloat the sort dataset, and because it might cause
+  * unexpected output order if the sort isn't stable.  Expensive expressions
+  * are postponed if there is a LIMIT, or if root->tuple_fraction shows that
+  * partial evaluation of the query is possible (if neither is true, we expect
+  * to have to evaluate the expressions for every row anyway), or if there are
+  * any volatile or set-returning expressions (since once we've put in a
+  * projection at all, it won't cost any more to postpone more stuff).
+  *
+  * Another issue that could potentially be considered here is that
+  * evaluating tlist expressions could result in data that's either wider
+  * or narrower than the input Vars, thus changing the volume of data that
+  * has to go through the Sort.  However, we usually have only a very bad
+  * idea of the output width of any expression more complex than a Var,
+  * so for now it seems too risky to try to optimize on that basis.
+  *
+  * Note that if we do produce a modified sort-input target, and then the
+  * query ends up not using an explicit Sort, no particular harm is done:
+  * we'll initially use the modified target for the preceding path nodes,
+  * but then change them to the final target with apply_projection_to_path.
+  * Moreover, in such a case the guarantees about evaluation order of
+  * volatile functions still hold, since the rows are sorted already.
+  *
+  * This function has some things in common with make_group_input_target and
+  * make_window_input_target, though the detailed rules for what to do are
+  * different.  We never flatten/postpone any grouping or ordering columns;
+  * those are needed before the sort.  If we do flatten a particular
+  * expression, we leave Aggref and WindowFunc nodes alone, since those were
+  * computed earlier.
+  *
+  * 'final_target' is the query's final target list (in PathTarget form)
+  * 'have_postponed_srfs' is an output argument, see below
+  *
+  * The result is the PathTarget to be computed by the plan node immediately
+  * below the Sort step (and the Distinct step, if any).  This will be
+  * exactly final_target if we decide a projection step wouldn't be helpful.
+  *
+  * In addition, *have_postponed_srfs is set to TRUE if we choose to postpone
+  * any set-returning functions to after the Sort.
+  */
+ static PathTarget *
+ make_sort_input_target(PlannerInfo *root,
+                        PathTarget *final_target,
+                        bool *have_postponed_srfs)
+ {
+     Query       *parse = root->parse;
+     PathTarget *input_target;
+     int            ncols;
+     bool       *postpone_col;
+     bool        have_srf;
+     bool        have_volatile;
+     bool        have_expensive;
+     List       *postponable_cols;
+     List       *postponable_vars;
+     int            i;
+     ListCell   *lc;
+
+     /* Shouldn't get here unless query has ORDER BY */
+     Assert(parse->sortClause);
+
+     *have_postponed_srfs = false;        /* default result */
+
+     /* Inspect tlist and collect per-column information */
+     ncols = list_length(final_target->exprs);
+     postpone_col = (bool *) palloc0(ncols * sizeof(bool));
+     have_srf = have_volatile = have_expensive = false;
+
+     i = 0;
+     foreach(lc, final_target->exprs)
+     {
+         Expr       *expr = (Expr *) lfirst(lc);
+
+         /*
+          * If the column has a sortgroupref, assume it has to be evaluated
+          * before sorting.  Generally such columns would be ORDER BY, GROUP
+          * BY, etc targets.  One exception is columns that were removed from
+          * GROUP BY by remove_useless_groupby_columns() ... but those would
+          * only be Vars anyway.  There don't seem to be any cases where it
+          * would be worth the trouble to double-check.
+          */
+         if (final_target->sortgrouprefs[i] == 0)
+         {
+             /*
+              * If it returns a set or is volatile, that's an unconditional
+              * reason to postpone.  Check the SRF case first because we must
+              * know whether we have any postponed SRFs.
+              */
+             if (expression_returns_set((Node *) expr))
+             {
+                 postpone_col[i] = true;
+                 have_srf = true;
+             }
+             else if (contain_volatile_functions((Node *) expr))
+             {
+                 postpone_col[i] = true;
+                 have_volatile = true;
+             }
+             else
+             {
+                 /*
+                  * Else check the cost.  XXX it's annoying to have to do this
+                  * when set_pathtarget_cost_width() just did it.  Refactor to
+                  * allow sharing the work?
+                  */
+                 QualCost    cost;
+
+                 cost_qual_eval_node(&cost, (Node *) expr, root);
+
+                 /*
+                  * We arbitrarily define "expensive" as "more than 10X
+                  * cpu_operator_cost".  Note this will take in any PL function
+                  * with default cost.
+                  */
+                 if (cost.per_tuple > 10 * cpu_operator_cost)
+                 {
+                     postpone_col[i] = true;
+                     have_expensive = true;
+                 }
+             }
+         }
+         i++;
+     }
+
+     /*
+      * If we don't need a post-sort projection, just return final_target.
+      */
+     if (!(have_srf || have_volatile ||
+           (have_expensive &&
+            (parse->limitCount || root->tuple_fraction > 0))))
+         return final_target;
+
+     /*
+      * Report whether the post-sort projection will contain set-returning
+      * functions.  This is important because it affects whether the Sort can
+      * rely on the query's LIMIT (if any) to bound the number of rows it needs
+      * to return.
+      */
+     *have_postponed_srfs = have_srf;
+
+     /*
+      * Construct the sort-input target, taking all non-postponable columns and
+      * then adding Vars, PlaceHolderVars, Aggrefs, and WindowFuncs found in
+      * the postponable ones.
+      */
+     input_target = create_empty_pathtarget();
+     postponable_cols = NIL;
+
+     i = 0;
+     foreach(lc, final_target->exprs)
+     {
+         Expr       *expr = (Expr *) lfirst(lc);
+
+         if (postpone_col[i])
+             postponable_cols = lappend(postponable_cols, expr);
+         else
+             add_column_to_pathtarget(input_target, expr,
+                                      final_target->sortgrouprefs[i]);
+
+         i++;
+     }
+
+     /*
+      * Pull out all the Vars, Aggrefs, and WindowFuncs mentioned in
+      * postponable columns, and add them to the sort-input target if not
+      * already present.  (Some might be there already.)  We mustn't
+      * deconstruct Aggrefs or WindowFuncs here, since the projection node
+      * would be unable to recompute them.
+      */
+     postponable_vars = pull_var_clause((Node *) postponable_cols,
+                                        PVC_INCLUDE_AGGREGATES |
+                                        PVC_INCLUDE_WINDOWFUNCS |
+                                        PVC_INCLUDE_PLACEHOLDERS);
+     add_new_columns_to_pathtarget(input_target, postponable_vars);
+
+     /* clean up cruft */
+     list_free(postponable_vars);
+     list_free(postponable_cols);
+
+     /* XXX this represents even more redundant cost calculation ... */
+     return set_pathtarget_cost_width(root, input_target);
+ }
+
+ /*
   * get_cheapest_fractional_path
   *      Find the cheapest path for retrieving a specified fraction of all
   *      the tuples expected to be returned by the given relation.
diff --git a/src/backend/optimizer/util/tlist.c b/src/backend/optimizer/util/tlist.c
index cbc8c2b..9f85dee 100644
*** a/src/backend/optimizer/util/tlist.c
--- b/src/backend/optimizer/util/tlist.c
*************** copy_pathtarget(PathTarget *src)
*** 624,629 ****
--- 624,640 ----
  }

  /*
+  * create_empty_pathtarget
+  *      Create an empty (zero columns, zero cost) PathTarget.
+  */
+ PathTarget *
+ create_empty_pathtarget(void)
+ {
+     /* This is easy, but we don't want callers to hard-wire this ... */
+     return (PathTarget *) palloc0(sizeof(PathTarget));
+ }
+
+ /*
   * add_column_to_pathtarget
   *        Append a target column to the PathTarget.
   *
*************** add_column_to_pathtarget(PathTarget *tar
*** 656,661 ****
--- 667,707 ----
  }

  /*
+  * add_new_column_to_pathtarget
+  *        Append a target column to the PathTarget, but only if it's not
+  *        equal() to any pre-existing target expression.
+  *
+  * The caller cannot specify a sortgroupref, since it would be unclear how
+  * to merge that with a pre-existing column.
+  *
+  * As with make_pathtarget_from_tlist, we leave it to the caller to update
+  * the cost and width fields.
+  */
+ void
+ add_new_column_to_pathtarget(PathTarget *target, Expr *expr)
+ {
+     if (!list_member(target->exprs, expr))
+         add_column_to_pathtarget(target, expr, 0);
+ }
+
+ /*
+  * add_new_columns_to_pathtarget
+  *        Apply add_new_column_to_pathtarget() for each element of the list.
+  */
+ void
+ add_new_columns_to_pathtarget(PathTarget *target, List *exprs)
+ {
+     ListCell   *lc;
+
+     foreach(lc, exprs)
+     {
+         Expr       *expr = (Expr *) lfirst(lc);
+
+         add_new_column_to_pathtarget(target, expr);
+     }
+ }
+
+ /*
   * apply_pathtarget_labeling_to_tlist
   *        Apply any sortgrouprefs in the PathTarget to matching tlist entries
   *
diff --git a/src/include/optimizer/tlist.h b/src/include/optimizer/tlist.h
index 2c79a80..0d745a0 100644
*** a/src/include/optimizer/tlist.h
--- b/src/include/optimizer/tlist.h
*************** extern bool grouping_is_hashable(List *g
*** 55,62 ****
--- 55,65 ----
  extern PathTarget *make_pathtarget_from_tlist(List *tlist);
  extern List *make_tlist_from_pathtarget(PathTarget *target);
  extern PathTarget *copy_pathtarget(PathTarget *src);
+ extern PathTarget *create_empty_pathtarget(void);
  extern void add_column_to_pathtarget(PathTarget *target,
                           Expr *expr, Index sortgroupref);
+ extern void add_new_column_to_pathtarget(PathTarget *target, Expr *expr);
+ extern void add_new_columns_to_pathtarget(PathTarget *target, List *exprs);
  extern void apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target);

  /* Convenience macro to get a PathTarget with valid cost/width fields */

В списке pgsql-hackers по дате отправления:

Предыдущее
От: Dilip Kumar
Дата:
Сообщение: Re: Relation extension scalability
Следующее
От: Petr Jelinek
Дата:
Сообщение: Re: Relation extension scalability