Re: [HACKERS] Optimizer questions

```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);
+
}
}
*************** 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
+ 									 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
+ 	 * 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);
+
+ 	/* 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));
+ }
+
+ /*
*		Append a target column to the PathTarget.
*
*** 656,661 ****
--- 667,707 ----
}

/*
+  *		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
+ {
+ 	if (!list_member(target->exprs, expr))
+ }
+
+ /*
+  *		Apply add_new_column_to_pathtarget() for each element of the list.
+  */
+ void
+ {
+ 	ListCell   *lc;
+
+ 	foreach(lc, exprs)
+ 	{
+ 		Expr	   *expr = (Expr *) lfirst(lc);
+
+ 	}
+ }
+
+ /*
* 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);
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 */
```
```--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
```