Super PathKeys (Allowing sort order through precision loss functions)

Started by David Rowleyabout 7 years ago16 messages
#1David Rowley
david.rowley@2ndquadrant.com
1 attachment(s)

Dear Hackers,

I've started working on something I've ended up calling "Super
PathKeys". The idea here is to increase the likelihood of a Path with
PathKeys being used for a purpose that requires a less strict sort
order due to ordering being required from the return value of some
precision loss function.

This is best explained by example, so here's the actual and expected
output of some tests that the patch adds:

create table tstbl (ts timestamp, a int);
create index on tstbl (ts, a);
set enable_sort = 0;
explain (costs off) select date_trunc('year', ts), a, count(*) from
tstbl group by 1,2 order by 1,2;
QUERY PLAN
-----------------------------------------------------
GroupAggregate
Group Key: date_trunc('year'::text, ts), a
-> Index Only Scan using tstbl_ts_a_idx on tstbl
(3 rows)

-- Test a more complex case where the superkey can be matched to
multiple pathkeys
explain (costs off) select date_trunc('year', ts), date_trunc('month',
ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
QUERY PLAN
-----------------------------------------------------------------------------
GroupAggregate
Group Key: date_trunc('year'::text, ts), date_trunc('month'::text, ts), a
-> Index Only Scan using tstbl_ts_a_idx on tstbl
(3 rows)

You can see here that the planner managed to make use of the index on
(ts, a) to provide sorted input to the GROUP BY date_trunc('year',
ts). This saves a possible slow HashAggregate plan, which would be
especially bad if there are a large number of groups. It'll also be
very useful if only a subset of all groups are required and a LIMIT
clause is specified. It should also be useful for reports run on large
timeseries data tables, especially partitioned ones when teamed up
with the "Ordered Partitioned Table Scans" in [1]https://commitfest.postgresql.org/20/1850/.

Benchmarks don't seem so relevant here as it would be simple enough to
craft them to show anything between 0% improvement up to some number
that's difficult to pronounce. I think it's better to discuss how I
think this is possible.

Implementation:

I've modified pg_proc to add a new column which optionally can be set
to the argument number of the input parameter that the return value
will be calculated from. In the patch, I've only set this so far for
the date_trunc() function and a handful of casting functions. When
building a PathKey the expression used in the key is passed into a
function which tries to extract the superkey expr. If a non-NULL value
is returned then an additional PathKey is generated with that expr and
that's set in a new field named pk_superkey in the original PathKey.
If the superkey expr itself contains another superkey expr then the
superkey PathKey just itself has a link to another superkey, and so
on.

pathkeys_contained_in() has been modified so that when a non-matching
PathKey is found it tries again by looking at the pk_superkey (if set)
and keeps walking up the chain of PathKeys until it reaches a PathKey
without a pk_superkey, or it finds a match. The function also must
check if the next key1 matches the same key2. This allows the 2nd
example above to work since both date_trunc() calls match the "ts" key
in the index scan. If the 2nd attempt does not match then we just
backup one and move to the next key2 and repeat.

I think this functionality could also be used to allow superkeys to be
derived through casts. We'd need to be careful to only tag casting
functions where we're certain the sort order of the input and output
types match. In the patch, I've just tagged a couple of casting
functions between timestamp and date, but I imagine it should also be
possible for casts between all the INT types, probably in either
direction. More careful thought would be needed for other numerical
types and text types, I imagine, though I've not thought much about
that.

Syntax:

I've not really gone very far to think about exposing this to userland
yet. I've only thought as far as

CREATE FUNCTION name (ORDER KEY <p1name> <type>) RETURNS ...

with some checks done in CreateFunction() to ensure there's not more
than 1 ORDER KEY, and one is only specified for non-void functions.
"KEY" is unreserved but so is "BY", so I don't forsee any grammar
issues with the above, though I'm no expert.

Status of the patch:

The patch is very much still a proof of concept. I'm abusing some
parser level functions to find the correct sortop for the super
PathKey. There also might be some giant holes in the entire concept as
I only dreamed the entire thing during a weekend trip while about 1.5
mountains out of range of the internet. I only started experimenting
with the idea yesterday.

The reason I'm posting now is:

1. Transparency. I'm working on this.
2. Someone might point out a good reason why this can't be done.
3. Some testing or fuzz testing might reveal some bugs in the patch.

Please find attached the proof of concept patch.

Comments (good or bad) welcome. Reviews and testing is also welcome.

I'm considering adding it to the November 'fest. Likely I'll decided
that based on any feedback received before then.

The "superkey" name is borrowed from OOP. Think "superclass". If it
turns out not to be a dud, we can call it something else.

[1]: https://commitfest.postgresql.org/20/1850/

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

v1-0001-Allow-Pathkeys-to-derive-their-order-from-a-paren.patchapplication/octet-stream; name=v1-0001-Allow-Pathkeys-to-derive-their-order-from-a-paren.patchDownload
From 1f64246b95b1a691bf883c852b4d0363cc5439f7 Mon Sep 17 00:00:00 2001
From: "dgrowley@gmail.com" <dgrowley@gmail.com>
Date: Mon, 29 Oct 2018 20:25:53 +1300
Subject: [PATCH v1] Allow Pathkeys to derive their order from a parent key

This parent PathKey concept has been given the name "Super Keys". The term
"super" has been borrowed out of class hierarchy from OOP. A Path
containing a PathKey which has a pk_superkey set must also be ordered by
that super key.  The reverse is not true, meaning the super key describes
a more strict ordering than any subkey which uses it.

A new column has been added to pg_proc to allow an optional setting to
mention which of the function arguments are the order key.  Giving a
function a valid order key argument is only valid for precision loss
functions or functions which return an exactly equivalent value (e.g casts
to a wider type). Some examples are:  date_trunc() truncates precision
from
the 2nd argument, so the 2nd argument can become a super PathKey allowing
a btree index on that column to provide sorted input to a GROUP BY clause
containing that function, which would allow a GroupAggregate to be used
instead of a HashAggregate, which can improve performance significantly,
especially so when only a small subset of all groups are required.

The functionality is likely also useful for casts between types, however
when modifying existing casts functions to give them an order key care
must be taken to ensure the sort order of the input and output types match
exactly.
---
 doc/src/sgml/catalogs.sgml              |   8 ++
 src/backend/catalog/pg_proc.c           |   1 +
 src/backend/nodes/copyfuncs.c           |   2 +-
 src/backend/nodes/equalfuncs.c          |   1 +
 src/backend/nodes/outfuncs.c            |   1 +
 src/backend/optimizer/README            |  10 ++
 src/backend/optimizer/path/pathkeys.c   | 172 ++++++++++++++++++++++++++++++--
 src/backend/optimizer/plan/createplan.c |  18 +++-
 src/backend/utils/cache/lsyscache.c     |  18 ++++
 src/include/catalog/pg_class.dat        |   2 +-
 src/include/catalog/pg_proc.dat         |  10 +-
 src/include/catalog/pg_proc.h           |   6 ++
 src/include/nodes/relation.h            |   6 ++
 src/include/utils/lsyscache.h           |   1 +
 src/test/regress/expected/indexing.out  |  21 ++++
 src/test/regress/sql/indexing.sql       |  10 ++
 16 files changed, 270 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 9edba96fab..ef4ad9a43d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5304,6 +5304,14 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>Number of input arguments</entry>
      </row>
 
+     <row>
+      <entry><structfield>proorderkeyarg</structfield></entry>
+      <entry><type>int2</type></entry>
+      <entry></entry>
+      <entry>0-based number defining the input argument which the return value
+      of the function is ordered by.</entry>
+     </row>
+     
      <row>
       <entry><structfield>pronargdefaults</structfield></entry>
       <entry><type>int2</type></entry>
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 0c817047cd..2e15898c1e 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -326,6 +326,7 @@ ProcedureCreate(const char *procedureName,
 	values[Anum_pg_proc_provolatile - 1] = CharGetDatum(volatility);
 	values[Anum_pg_proc_proparallel - 1] = CharGetDatum(parallel);
 	values[Anum_pg_proc_pronargs - 1] = UInt16GetDatum(parameterCount);
+	values[Anum_pg_proc_proorderkeyarg - 1] = Int16GetDatum(-1);
 	values[Anum_pg_proc_pronargdefaults - 1] = UInt16GetDatum(list_length(parameterDefaults));
 	values[Anum_pg_proc_prorettype - 1] = ObjectIdGetDatum(returnType);
 	values[Anum_pg_proc_proargtypes - 1] = PointerGetDatum(parameterTypes);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index e8ea59e34a..13fe15f7cb 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2216,7 +2216,7 @@ _copyPathKey(const PathKey *from)
 	COPY_SCALAR_FIELD(pk_opfamily);
 	COPY_SCALAR_FIELD(pk_strategy);
 	COPY_SCALAR_FIELD(pk_nulls_first);
-
+	COPY_NODE_FIELD(pk_superkey);
 	return newnode;
 }
 
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3bb91c9595..69ea95d1f6 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -824,6 +824,7 @@ _equalPathKey(const PathKey *a, const PathKey *b)
 	COMPARE_SCALAR_FIELD(pk_opfamily);
 	COMPARE_SCALAR_FIELD(pk_strategy);
 	COMPARE_SCALAR_FIELD(pk_nulls_first);
+	COMPARE_NODE_FIELD(pk_superkey);
 
 	return true;
 }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 69731ccdea..54a69272f2 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2490,6 +2490,7 @@ _outPathKey(StringInfo str, const PathKey *node)
 	WRITE_OID_FIELD(pk_opfamily);
 	WRITE_INT_FIELD(pk_strategy);
 	WRITE_BOOL_FIELD(pk_nulls_first);
+	WRITE_NODE_FIELD(pk_superkey);
 }
 
 static void
diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README
index 9c852a15ef..531409a540 100644
--- a/src/backend/optimizer/README
+++ b/src/backend/optimizer/README
@@ -553,6 +553,7 @@ the result.  Each PathKey contains these fields:
 	* a btree opfamily OID (must match one of those in the EC)
 	* a sort direction (ascending or descending)
 	* a nulls-first-or-last flag
+	* an optional link to a "super" pathkey.
 
 The EquivalenceClass represents the value being sorted on.  Since the
 various members of an EquivalenceClass are known equal according to the
@@ -667,6 +668,15 @@ if a path was sorted by {a.x} below an outer join, we'll re-sort if that
 sort ordering was important; and so using the same PathKey for both sort
 orderings doesn't create any real problem.
 
+Pathkeys may also contain an optional "super key". The super key is a link
+to another pathkey to which this pathkey's order is derived from.  For
+example a pathkey with the expression "date_trunc('hour', timestamp)"
+could contain a super key containing the expression "timestamp".  This is
+because this particular function truncates away precision, meaning any
+Path which is ordered by "timestamp" is also ordered by
+date_trunc('hour', timestamp).  When checking if a set of path keys is
+contained in another set we make use of any super keys to increase the
+likelihood of a match.
 
 Order of processing for EquivalenceClasses and PathKeys
 -------------------------------------------------------
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index ec66cb9c3c..fd34f93648 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -25,10 +25,18 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/tlist.h"
+#include "parser/parse_oper.h" /* XXX shouldn't be included here */
 #include "utils/lsyscache.h"
 
 
 static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys);
+static PathKey *make_pathkey_from_sortop(PlannerInfo *root,
+						 Expr *expr,
+						 Relids nullable_relids,
+						 Oid ordering_op,
+						 bool nulls_first,
+						 Index sortref,
+						 bool create_it);
 static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
 
 
@@ -146,6 +154,30 @@ pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys)
 	return false;
 }
 
+/*
+ * extract_superkey_expr
+ *		Analyze 'expr' and attempt to determine if the expr has a valid
+ *		super key, if so return it, else return NULL.
+ */
+static Expr *
+extract_superkey_expr(const Expr *expr)
+{
+	if (IsA(expr, FuncExpr))
+	{
+		FuncExpr   *func = (FuncExpr *)expr;
+		int16		orderkeyarg = get_func_orderkeyarg(func->funcid);
+
+		if (orderkeyarg >= 0)
+		{
+			Assert(orderkeyarg < list_length(func->args));
+
+			return list_nth(func->args, orderkeyarg);
+		}
+	}
+
+	return NULL;
+}
+
 /*
  * make_pathkey_from_sortinfo
  *	  Given an expression and sort-order information, create a PathKey.
@@ -179,10 +211,12 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 						   Relids rel,
 						   bool create_it)
 {
+	PathKey	   *pathkey;
 	int16		strategy;
 	Oid			equality_op;
 	List	   *opfamilies;
 	EquivalenceClass *eclass;
+	Expr	   *super_expr;
 
 	strategy = reverse_sort ? BTGreaterStrategyNumber : BTLessStrategyNumber;
 
@@ -214,8 +248,39 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 		return NULL;
 
 	/* And finally we can find or create a PathKey node */
-	return make_canonical_pathkey(root, eclass, opfamily,
-								  strategy, nulls_first);
+	pathkey = make_canonical_pathkey(root, eclass, opfamily,
+									 strategy, nulls_first);
+
+	/* Determine if 'expr' contains any super keys */
+	super_expr = extract_superkey_expr(expr);
+
+	if (super_expr != NULL)
+	{
+		Oid			sortop;
+		Oid			eqop;
+		bool		hashable;
+
+		if (pathkey->pk_strategy == BTLessStrategyNumber)
+			get_sort_group_operators(exprType((Node *) super_expr), /* XXX this is not the right function to call */
+									 true, true, false,
+									 &sortop, &eqop, NULL,
+									 &hashable);
+		else
+			get_sort_group_operators(exprType((Node *) super_expr),
+									 false, true, true,
+									 NULL, &eqop, &sortop,
+									 &hashable);
+
+		pathkey->pk_superkey = make_pathkey_from_sortop(root,
+														super_expr,
+														nullable_relids,
+														sortop,
+														nulls_first,
+														sortref,
+														create_it);
+	}
+
+	return pathkey;
 }
 
 /*
@@ -311,20 +376,107 @@ compare_pathkeys(List *keys1, List *keys2)
 /*
  * pathkeys_contained_in
  *	  Common special case of compare_pathkeys: we just want to know
- *	  if keys2 are at least as well sorted as keys1.
+ *	  if keys2 are at least as well sorted as keys1. keys1 can exploit any
+ *	  super keys to determine if the order matches.
  */
 bool
 pathkeys_contained_in(List *keys1, List *keys2)
 {
-	switch (compare_pathkeys(keys1, keys2))
+	ListCell   *key1,
+			   *key2;
+
+	/*
+	 * Fall out quickly if we are passed two identical lists.  This mostly
+	 * catches the case where both are NIL, but that's common enough to
+	 * warrant the test.
+	 */
+	if (keys1 == keys2)
+		return true;
+
+	key1 = list_head(keys1);
+
+	foreach(key2, keys2)
 	{
-		case PATHKEYS_EQUAL:
-		case PATHKEYS_BETTER2:
-			return true;
-		default:
-			break;
+		PathKey    *pathkey1;
+		PathKey    *pathkey2 = (PathKey *) lfirst(key2);
+		bool		first = true;
+
+		for (;;)
+		{
+			if (key1 == NULL)
+				goto out;
+
+			pathkey1 = (PathKey *) lfirst(key1);
+
+			if (pathkey1 != pathkey2)
+			{
+				bool		found = false;
+
+				/*
+				 * No match on the main key... see if any super keys exist
+				 * which do match.
+				 */
+
+				pathkey1 = pathkey1->pk_superkey;
+				while (pathkey1 != NULL)
+				{
+					if (pathkey1 == pathkey2)
+					{
+						found = true;
+						break;
+					}
+					pathkey1 = pathkey1->pk_superkey;
+				}
+
+				if (found)
+				{
+					/*
+					 * When we find a matching super key we must try to match
+					 * the next pathkey1 to the same pathkey2. There may be
+					 * multiple pathkey1s relying on this pathkey2.  If we
+					 * don't find a subsequent match then we can just try to
+					 * match to the next pathkey2.
+					 */
+					key1 = lnext(key1);
+					first = false;
+					continue;
+				}
+				else if (first)
+				{
+					/*
+					 * Lack of match is only a problem when its the first
+					 * attempt to match fails
+					 */
+					return false;
+				}
+				else
+				{
+					/*
+					 * Not found, but we've already matched to this key1, so
+					 * skip to next key2.
+					 */
+					break;
+				}
+			}
+			else
+			{
+				key1 = lnext(key1);
+				/* main key matched, just skip to next key2. */
+				break;
+			}
+		}
 	}
-	return false;
+
+out:
+	/*
+	 * If we reached the end of only one list, the other is longer and
+	 * therefore not a subset.
+	 */
+	if (key1 != NULL)
+		return false;	/* key1 is longer */
+	if (key2 != NULL)
+		return true;	/* key2 is longer */
+	return true;
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ae46b0140e..015ecc2244 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -5734,6 +5734,7 @@ prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
 			 * WindowFunc in a sort expression, treat it as a variable.
 			 */
 			Expr	   *sortexpr = NULL;
+			Oid			em_datatype = InvalidOid;
 
 			foreach(j, ec->ec_members)
 			{
@@ -5758,6 +5759,7 @@ prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
 					continue;
 
 				sortexpr = em->em_expr;
+				em_datatype = em->em_datatype;
 				exprvars = pull_var_clause((Node *) sortexpr,
 										   PVC_INCLUDE_AGGREGATES |
 										   PVC_INCLUDE_WINDOWFUNCS |
@@ -5775,7 +5777,21 @@ prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
 				}
 			}
 			if (!j)
-				elog(ERROR, "could not find pathkey item to sort");
+			{
+				/*
+				 * Hard error if we were unable to find an EquivalenceMember
+				 * in order to determine the data type of the sort key
+				 */
+				if (sortexpr == NULL)
+					elog(ERROR, "could not find pathkey item to sort");
+
+				/*
+				 * Otherwise just use the datatype from the EquivalenceMember
+				 * and we'll add a new target list item for the sortexpr
+				 * below.
+				 */
+				pk_datatype = em_datatype;
+			}
 
 			/*
 			 * Do we need to insert a Result node?
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 892ddc0d48..1ce373fdd4 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1440,6 +1440,24 @@ get_func_nargs(Oid funcid)
 	ReleaseSysCache(tp);
 	return result;
 }
+/*
+ * get_func_orderkeyarg
+ *	Given procedure id, return the 0-based order key arg number.
+ */
+int16
+get_func_orderkeyarg(Oid funcid)
+{
+	HeapTuple	tp;
+	int16		result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->proorderkeyarg;
+	ReleaseSysCache(tp);
+	return result;
+}
 
 /*
  * get_func_signature
diff --git a/src/include/catalog/pg_class.dat b/src/include/catalog/pg_class.dat
index 9fffdef379..48ab7b2ec8 100644
--- a/src/include/catalog/pg_class.dat
+++ b/src/include/catalog/pg_class.dat
@@ -47,7 +47,7 @@
   reloftype => '0', relowner => 'PGUID', relam => '0', relfilenode => '0',
   reltablespace => '0', relpages => '0', reltuples => '0', relallvisible => '0',
   reltoastrelid => '0', relhasindex => 'f', relisshared => 'f',
-  relpersistence => 'p', relkind => 'r', relnatts => '28', relchecks => '0',
+  relpersistence => 'p', relkind => 'r', relnatts => '29', relchecks => '0',
   relhasoids => 't', relhasrules => 'f', relhastriggers => 'f',
   relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f',
   relispopulated => 't', relreplident => 'n', relispartition => 'f',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4d7fe1b383..b41384cba5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2198,7 +2198,7 @@
   proargtypes => 'text interval', prosrc => 'interval_part' },
 { oid => '1174', descr => 'convert date to timestamp with time zone',
   proname => 'timestamptz', provolatile => 's', prorettype => 'timestamptz',
-  proargtypes => 'date', prosrc => 'date_timestamptz' },
+  proargtypes => 'date', prosrc => 'date_timestamptz', proorderkeyarg => 0 },
 { oid => '2711',
   descr => 'promote groups of 24 hours to numbers of days and promote groups of 30 days to numbers of months',
   proname => 'justify_interval', prorettype => 'interval',
@@ -2279,7 +2279,8 @@
 { oid => '1217',
   descr => 'truncate timestamp with time zone to specified units',
   proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
-  proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc' },
+  proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc',
+  proorderkeyarg => 1 },
 { oid => '1218', descr => 'truncate interval to specified units',
   proname => 'date_trunc', prorettype => 'interval',
   proargtypes => 'text interval', prosrc => 'interval_trunc' },
@@ -5470,13 +5471,14 @@
   proargtypes => 'timestamptz', prosrc => 'timestamptz_time' },
 { oid => '2020', descr => 'truncate timestamp to specified units',
   proname => 'date_trunc', prorettype => 'timestamp',
-  proargtypes => 'text timestamp', prosrc => 'timestamp_trunc' },
+  proargtypes => 'text timestamp', prosrc => 'timestamp_trunc',
+  proorderkeyarg => 1 },
 { oid => '2021', descr => 'extract field from timestamp',
   proname => 'date_part', prorettype => 'float8',
   proargtypes => 'text timestamp', prosrc => 'timestamp_part' },
 { oid => '2024', descr => 'convert date to timestamp',
   proname => 'timestamp', prorettype => 'timestamp', proargtypes => 'date',
-  prosrc => 'date_timestamp' },
+  prosrc => 'date_timestamp', proorderkeyarg => 0 },
 { oid => '2025', descr => 'convert date and time to timestamp',
   proname => 'timestamp', prorettype => 'timestamp', proargtypes => 'date time',
   prosrc => 'datetime_timestamp' },
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index a34b2596fa..718a21a003 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -79,6 +79,12 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce
 	/* Note: need not be given in pg_proc.dat; genbki.pl will compute it */
 	int16		pronargs;
 
+	/*
+	 * 0-based argument number of the argument that  defines the sort order
+	 * of the return value, or -1 when the function does not support this.
+	 */
+	int16		proorderkeyarg BKI_DEFAULT(-1);
+
 	/* number of arguments with defaults */
 	int16		pronargdefaults BKI_DEFAULT(0);
 
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 88d37236f7..c5e27784e1 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -977,6 +977,10 @@ typedef struct EquivalenceMember
  * equivalent and closely-related orderings. (See optimizer/README for more
  * information.)
  *
+ * PathKeys may also have a "super key". If present describes that the order
+ * described by the key can be satisfied by a path which is ordered by its
+ * 'pk_superkey'.  A super key may in turn have its own super key defined.
+ *
  * Note: pk_strategy is either BTLessStrategyNumber (for ASC) or
  * BTGreaterStrategyNumber (for DESC).  We assume that all ordering-capable
  * index types will use btree-compatible strategy numbers.
@@ -989,6 +993,8 @@ typedef struct PathKey
 	Oid			pk_opfamily;	/* btree opfamily defining the ordering */
 	int			pk_strategy;	/* sort direction (ASC or DESC) */
 	bool		pk_nulls_first; /* do NULLs come before normal values? */
+	struct PathKey *pk_superkey;	/* Link to path key which induces this
+									 * pathkey. */
 } PathKey;
 
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index ff1705ad2b..9096442b41 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -111,6 +111,7 @@ extern char *get_func_name(Oid funcid);
 extern Oid	get_func_namespace(Oid funcid);
 extern Oid	get_func_rettype(Oid funcid);
 extern int	get_func_nargs(Oid funcid);
+extern int16 get_func_orderkeyarg(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
 extern Oid	get_func_variadictype(Oid funcid);
 extern bool get_func_retset(Oid funcid);
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index ca27346f18..93c71defca 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -1404,3 +1404,24 @@ insert into covidxpart values (4, 1);
 insert into covidxpart values (4, 1);
 ERROR:  duplicate key value violates unique constraint "covidxpart4_a_b_idx"
 DETAIL:  Key (a)=(4) already exists.
+-- Test super path keys
+create table tstbl (ts timestamp, a int);
+create index on tstbl (ts, a);
+set enable_sort = 0;
+explain (costs off) select date_trunc('year', ts), a, count(*) from tstbl group by 1,2 order by 1,2;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ GroupAggregate
+   Group Key: date_trunc('year'::text, ts), a
+   ->  Index Only Scan using tstbl_ts_a_idx on tstbl
+(3 rows)
+
+-- Test a more complex case where the superkey can be matched to multiple pathkeys
+explain (costs off) select date_trunc('year', ts), date_trunc('month', ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ GroupAggregate
+   Group Key: date_trunc('year'::text, ts), date_trunc('month'::text, ts), a
+   ->  Index Only Scan using tstbl_ts_a_idx on tstbl
+(3 rows)
+
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index 400b7eb7ba..7d23c1cb27 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -753,3 +753,13 @@ create unique index on covidxpart4 (a);
 alter table covidxpart attach partition covidxpart4 for values in (4);
 insert into covidxpart values (4, 1);
 insert into covidxpart values (4, 1);
+
+-- Test super path keys
+create table tstbl (ts timestamp, a int);
+create index on tstbl (ts, a);
+set enable_sort = 0;
+
+explain (costs off) select date_trunc('year', ts), a, count(*) from tstbl group by 1,2 order by 1,2;
+
+-- Test a more complex case where the superkey can be matched to multiple pathkeys
+explain (costs off) select date_trunc('year', ts), date_trunc('month', ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
-- 
2.16.2.windows.1

#2Simon Riggs
simon@2ndquadrant.com
In reply to: David Rowley (#1)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On Tue, 30 Oct 2018 at 07:58, David Rowley <david.rowley@2ndquadrant.com>
wrote:

I've started working on something I've ended up calling "Super
PathKeys". The idea here is to increase the likelihood of a Path with
PathKeys being used for a purpose that requires a less strict sort
order due to ordering being required from the return value of some
precision loss function.

Anything left anchored would benefit, so SUBSTR(), TRIM() etc

Main use for this would be where the partition condition is a function, so
we can still order by partitions easily.

--
Simon Riggs http://www.2ndQuadrant.com/
<http://www.2ndquadrant.com/&gt;
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Rowley (#1)
Re: Super PathKeys (Allowing sort order through precision loss functions)

David Rowley <david.rowley@2ndquadrant.com> writes:

I've started working on something I've ended up calling "Super
PathKeys". The idea here is to increase the likelihood of a Path with
PathKeys being used for a purpose that requires a less strict sort
order due to ordering being required from the return value of some
precision loss function.

I'm a little confused by the terminology here, or why this has anything
at all to do with a new sort of pathkey. It seems like the idea you're
driving at is to be able to mark a function as being order-preserving,
in the sense that if one input is known sorted then the output will
also be sorted (by the same or a related opclass). You probably need
some additional constraints, like any other inputs being constants,
before that really works. But given that, I don't see why you need a
new kind of pathkey: you just have a new way to conclude that some
path is sorted by the pathkey you already want.

Maybe if I read the patch it'd be clearer why you want to describe it
that way, but I'm too lazy to do that right now. One thing I would
say though is that a pg_proc column identifying the interesting input
parameter is insufficient; you'd need to *explicitly* say which opclass(es)
the sorting behavior guarantee applies for.

-- Test a more complex case where the superkey can be matched to
multiple pathkeys
explain (costs off) select date_trunc('year', ts), date_trunc('month',
ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
QUERY PLAN
-----------------------------------------------------------------------------
GroupAggregate
Group Key: date_trunc('year'::text, ts), date_trunc('month'::text, ts), a
-> Index Only Scan using tstbl_ts_a_idx on tstbl
(3 rows)

[ squint... ] Does that really work? If so, how? It would need a whole
lot more knowledge about the behavior of date_trunc than I think could
possibly be reasonable to encode in a general mechanism.

regards, tom lane

#4David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#3)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 31 October 2018 at 08:52, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I've started working on something I've ended up calling "Super
PathKeys". The idea here is to increase the likelihood of a Path with
PathKeys being used for a purpose that requires a less strict sort
order due to ordering being required from the return value of some
precision loss function.

I'm a little confused by the terminology here, or why this has anything
at all to do with a new sort of pathkey. It seems like the idea you're
driving at is to be able to mark a function as being order-preserving,
in the sense that if one input is known sorted then the output will
also be sorted (by the same or a related opclass). You probably need
some additional constraints, like any other inputs being constants,
before that really works. But given that, I don't see why you need a
new kind of pathkey: you just have a new way to conclude that some
path is sorted by the pathkey you already want.

Thanks for chipping in on this.

The additional pathkeys are not required to make it work, they're just
required to make it work efficiently. The fact that we could to the
trouble of making pathkeys canonical so we can perform pointer
comparison rather than using equals() says to me that I'd better not
do anything to slow this down too much. Doing it without the superkey
idea seems to require quite a bit of analysis during
pathkeys_contained_in() as we'd need to check for superkeys then,
instead of when we're building the pathkey in the first place. As
for the code that I did add to pathkeys_contained_in(), I doubt it's
measurably slower for the normal case.

The other fields being Const part I did miss. That will also be a
requirement. I just failed to consider that date_trunc() could be used
with a variable 1st arg.

Maybe if I read the patch it'd be clearer why you want to describe it
that way, but I'm too lazy to do that right now. One thing I would
say though is that a pg_proc column identifying the interesting input
parameter is insufficient; you'd need to *explicitly* say which opclass(es)
the sorting behavior guarantee applies for.

-- Test a more complex case where the superkey can be matched to
multiple pathkeys
explain (costs off) select date_trunc('year', ts), date_trunc('month',
ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
QUERY PLAN
-----------------------------------------------------------------------------
GroupAggregate
Group Key: date_trunc('year'::text, ts), date_trunc('month'::text, ts), a
-> Index Only Scan using tstbl_ts_a_idx on tstbl
(3 rows)

[ squint... ] Does that really work? If so, how? It would need a whole
lot more knowledge about the behavior of date_trunc than I think could
possibly be reasonable to encode in a general mechanism.

I'm not entirely certain we can always consume multiple matching keys
or if it has to be one for one. However, I get an empty diff from each
of the following two query pairs.

select date_trunc('month'::text,date),date_Trunc('year', date) from dt
order by dt;
select date_trunc('month'::text,date),date_Trunc('year', date) from dt
order by 1,2;

and

select date_trunc('year'::text,date),date_Trunc('month', date) from dt
order by dt;
select date_trunc('year'::text,date),date_Trunc('month', date) from dt
order by 1,2;

with setup:

create table dt (date date);
insert into dt select d from generate_series('2018-01-01',
'2018-12-31', '1 day'::interval) d;

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#5Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#4)
Re: Super PathKeys (Allowing sort order through precision loss functions)

Hi,

On 10/30/2018 11:41 PM, David Rowley wrote:

On 31 October 2018 at 08:52, Tom Lane <tgl@sss.pgh.pa.us> wrote:

David Rowley <david.rowley@2ndquadrant.com> writes:

I've started working on something I've ended up calling "Super
PathKeys". The idea here is to increase the likelihood of a Path with
PathKeys being used for a purpose that requires a less strict sort
order due to ordering being required from the return value of some
precision loss function.

I'm a little confused by the terminology here, or why this has anything
at all to do with a new sort of pathkey. It seems like the idea you're
driving at is to be able to mark a function as being order-preserving,
in the sense that if one input is known sorted then the output will
also be sorted (by the same or a related opclass). You probably need
some additional constraints, like any other inputs being constants,
before that really works. But given that, I don't see why you need a
new kind of pathkey: you just have a new way to conclude that some
path is sorted by the pathkey you already want.

Thanks for chipping in on this.

The additional pathkeys are not required to make it work, they're just
required to make it work efficiently. The fact that we could to the
trouble of making pathkeys canonical so we can perform pointer
comparison rather than using equals() says to me that I'd better not
do anything to slow this down too much. Doing it without the superkey
idea seems to require quite a bit of analysis during
pathkeys_contained_in() as we'd need to check for superkeys then,
instead of when we're building the pathkey in the first place. As
for the code that I did add to pathkeys_contained_in(), I doubt it's
measurably slower for the normal case.

The other fields being Const part I did miss. That will also be a
requirement. I just failed to consider that date_trunc() could be used
with a variable 1st arg.

Maybe if I read the patch it'd be clearer why you want to describe it
that way, but I'm too lazy to do that right now. One thing I would
say though is that a pg_proc column identifying the interesting input
parameter is insufficient; you'd need to *explicitly* say which opclass(es)
the sorting behavior guarantee applies for.

The other thing likely affecting this is locale / collation. Probably
not for date_trunc, but certainly for things like substr()/trim(),
mentioned by Simon upthread.

In some languages the rules are pretty complex, and there's no chance
it'll survive arbitrary substr() applied to the string. For example, in
Czech we mostly sort character-by-character, but "ch" is an exception
sorted in between "h" and "i".

So essentially "hhhh <= hchh <= hihh". Obviously, substr($1,0,3) cuts
the "ch" in half, changing the ordering:

create table x (y text collate "cs_CZ");
insert into x values ('hhhh'), ('hchh'), ('hihh');

test=# select y from x order by 1;
y
------
hhhh
hchh
hihh
(3 rows)

test=# select substr(y,0,3) from x order by 1;
substr
--------
hc
hh
hi
(3 rows)

I'm preeeeeeetty sure other languages have even funnier rules ...

-- Test a more complex case where the superkey can be matched to
multiple pathkeys
explain (costs off) select date_trunc('year', ts), date_trunc('month',
ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
QUERY PLAN
-----------------------------------------------------------------------------
GroupAggregate
Group Key: date_trunc('year'::text, ts), date_trunc('month'::text, ts), a
-> Index Only Scan using tstbl_ts_a_idx on tstbl
(3 rows)

[ squint... ] Does that really work? If so, how? It would need a whole
lot more knowledge about the behavior of date_trunc than I think could
possibly be reasonable to encode in a general mechanism.

I'm not entirely certain we can always consume multiple matching keys
or if it has to be one for one. However, I get an empty diff from each
of the following two query pairs.

select date_trunc('month'::text,date),date_Trunc('year', date) from dt
order by dt;
select date_trunc('month'::text,date),date_Trunc('year', date) from dt
order by 1,2;

and

select date_trunc('year'::text,date),date_Trunc('month', date) from dt
order by dt;
select date_trunc('year'::text,date),date_Trunc('month', date) from dt
order by 1,2;

with setup:

create table dt (date date);
insert into dt select d from generate_series('2018-01-01',
'2018-12-31', '1 day'::interval) d;

I'm mildly suspicious of this data set, because it's essentially
perfectly sorted/deterministic, and the Sort node won't have to do
anything. So perhaps it just works by chance.

Consider this instead:

create table dt (ts timestamp, x text);

insert into dt select * from (select d, (case when random() < 0.5 then
'month' else 'hour' end) from generate_series('2018-01-01'::timestamp,
'2018-12-31'::timestamp, '1 hour'::interval) d) as foo order by random();

test=# explain select date_trunc(x, ts) from dt order by 1;
QUERY PLAN
-------------------------------------------------------------
Sort (cost=729.18..751.02 rows=8737 width=8)
Sort Key: (date_trunc(x, ts))
-> Seq Scan on dt (cost=0.00..157.21 rows=8737 width=8)
(3 rows)

test=# select date_trunc(x, ts) from dt order by 1;

date_trunc
---------------------
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
... seems ok ...

test=# explain select date_trunc(x, ts) from dt order by ts;
QUERY PLAN
--------------------------------------------------------------
Sort (cost=729.18..751.02 rows=8737 width=16)
Sort Key: ts
-> Seq Scan on dt (cost=0.00..157.21 rows=8737 width=16)
(3 rows)

test=# select date_trunc(x, ts) from dt order by ts;

date_trunc
---------------------
2018-01-01 00:00:00
2018-01-01 01:00:00
2018-01-01 02:00:00
2018-01-01 00:00:00
2018-01-01 04:00:00
2018-01-01 05:00:00
2018-01-01 06:00:00
2018-01-01 07:00:00
2018-01-01 08:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 13:00:00
2018-01-01 14:00:00
2018-01-01 00:00:00
2018-01-01 16:00:00
2018-01-01 17:00:00
2018-01-01 18:00:00
2018-01-01 19:00:00
2018-01-01 20:00:00
2018-01-01 21:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-02 00:00:00
2018-01-01 00:00:00
2018-01-02 02:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-02 05:00:00
2018-01-02 06:00:00
2018-01-02 07:00:00
2018-01-02 08:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-02 13:00:00
... kaboooooom! ...

I suspect this runs into the date_trunc() precision parameter being
variable, which is something Tom mentioned as a potential issue.

The already-mentioned substr/trim are another example of this issue. Not
only must the parameters be constant (or generally using the same value
for all rows), but it may work for some values and fail for others.

For example substr($1,0,$2) might work, but substr($1,10,$2) likely not.
Similarly for trim().

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#6David Rowley
david.rowley@2ndquadrant.com
In reply to: Tomas Vondra (#5)
1 attachment(s)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 31 October 2018 at 14:23, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

The other thing likely affecting this is locale / collation. Probably
not for date_trunc, but certainly for things like substr()/trim(),
mentioned by Simon upthread.

In some languages the rules are pretty complex, and there's no chance
it'll survive arbitrary substr() applied to the string. For example, in
Czech we mostly sort character-by-character, but "ch" is an exception
sorted in between "h" and "i".

So essentially "hhhh <= hchh <= hihh". Obviously, substr($1,0,3) cuts
the "ch" in half, changing the ordering:

create table x (y text collate "cs_CZ");
insert into x values ('hhhh'), ('hchh'), ('hihh');

test=# select y from x order by 1;
y
------
hhhh
hchh
hihh
(3 rows)

test=# select substr(y,0,3) from x order by 1;
substr
--------
hc
hh
hi
(3 rows)

I'm preeeeeeetty sure other languages have even funnier rules ...

That's pretty interesting, but as mentioned in my initial email...
More careful thought would be needed for other numerical types and
text types, I imagine, though I've not thought much about that.

I don't really think trim() or substr() would ever work for the reason
that they don't always operate on a prefix of the string. What you've
mentioned seems to rule out LEFT().

I'm mildly suspicious of this data set, because it's essentially
perfectly sorted/deterministic, and the Sort node won't have to do
anything. So perhaps it just works by chance.

Consider this instead:

create table dt (ts timestamp, x text);

insert into dt select * from (select d, (case when random() < 0.5 then
'month' else 'hour' end) from generate_series('2018-01-01'::timestamp,
'2018-12-31'::timestamp, '1 hour'::interval) d) as foo order by random();

[...]

2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-02 13:00:00
... kaboooooom! ...

Yeah. This is an issue. Thanks for the test case. However, I
acknowledged that in my reply to Tom. I did overlook it, which was
completely down to lack of imagination on my part. I'd never
considered using date_trunc() without a const 1st argument before. It
seems simple enough to disable the optimisation in that case. I've
attached a patch which does that. Maybe that'll help us look beyond
this and focus on any other reasons why this is not possible.

It's also true that this diminishes the usefulness of the idea, but
part of the reason I've posting the idea so early after having thought
about it is precisely to see if this is going to float or sink.
Maybe we'll decide the scope of usefulness is so small that it's not
worth it, or that each function has such different requirements that
we can't reasonably make it work by adding a few columns to pg_proc.
I'm personally more interested in the cases that can work. I
understand there is no shortage of cases where it can't.

Giving that we require const arguments away from the orderkey, perhaps
it could be made to work for simple arithmetic OpExprs. I'm not sure
if the use cases are actually there for that sort of thing and I've
seen WHERE indexcol+0 = <n> used many times to disable the use of
indexes, so making pathkeys see through those might be more annoying
than useful... But it's a thought...

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

v2-0001-Allow-Pathkeys-to-derive-their-order-from-a-paren.patchapplication/octet-stream; name=v2-0001-Allow-Pathkeys-to-derive-their-order-from-a-paren.patchDownload
From 548a753d7af2eb1186bc745172decb2db3359266 Mon Sep 17 00:00:00 2001
From: "dgrowley@gmail.com" <dgrowley@gmail.com>
Date: Mon, 29 Oct 2018 20:25:53 +1300
Subject: [PATCH v2] Allow Pathkeys to derive their order from a parent key

This parent PathKey concept has been given the name "Super Keys". The term
"super" has been borrowed out of class hierarchy from OOP. A Path
containing a PathKey which has a pk_superkey set must also be ordered by
that super key.  The reverse is not true, meaning the super key describes
a more strict ordering than any subkey which uses it.

A new column has been added to pg_proc to allow an optional setting to
mention which of the function arguments are the order key.  Giving a
function a valid order key argument is only valid for precision loss
functions or functions which return an exactly equivalent value (e.g casts
to a wider type). Some examples are:  date_trunc() truncates precision
from
the 2nd argument, so the 2nd argument can become a super PathKey allowing
a btree index on that column to provide sorted input to a GROUP BY clause
containing that function, which would allow a GroupAggregate to be used
instead of a HashAggregate, which can improve performance significantly,
especially so when only a small subset of all groups are required.

The functionality is likely also useful for casts between types, however
when modifying existing casts functions to give them an order key care
must be taken to ensure the sort order of the input and output types match
exactly.
---
 doc/src/sgml/catalogs.sgml              |   8 ++
 src/backend/catalog/pg_proc.c           |   1 +
 src/backend/nodes/copyfuncs.c           |   2 +-
 src/backend/nodes/equalfuncs.c          |   1 +
 src/backend/nodes/outfuncs.c            |   1 +
 src/backend/optimizer/README            |  11 ++
 src/backend/optimizer/path/pathkeys.c   | 196 ++++++++++++++++++++++++++++++--
 src/backend/optimizer/plan/createplan.c |  18 ++-
 src/backend/utils/cache/lsyscache.c     |  18 +++
 src/include/catalog/pg_class.dat        |   2 +-
 src/include/catalog/pg_proc.dat         |  10 +-
 src/include/catalog/pg_proc.h           |   6 +
 src/include/nodes/relation.h            |   6 +
 src/include/utils/lsyscache.h           |   1 +
 src/test/regress/expected/indexing.out  |  34 ++++++
 src/test/regress/sql/indexing.sql       |  15 +++
 16 files changed, 313 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4256516c08..62c9db235b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5304,6 +5304,14 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>Number of input arguments</entry>
      </row>
 
+     <row>
+      <entry><structfield>proorderkeyarg</structfield></entry>
+      <entry><type>int2</type></entry>
+      <entry></entry>
+      <entry>0-based number defining the input argument which the return value
+      of the function is ordered by.</entry>
+     </row>
+     
      <row>
       <entry><structfield>pronargdefaults</structfield></entry>
       <entry><type>int2</type></entry>
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 0c817047cd..2e15898c1e 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -326,6 +326,7 @@ ProcedureCreate(const char *procedureName,
 	values[Anum_pg_proc_provolatile - 1] = CharGetDatum(volatility);
 	values[Anum_pg_proc_proparallel - 1] = CharGetDatum(parallel);
 	values[Anum_pg_proc_pronargs - 1] = UInt16GetDatum(parameterCount);
+	values[Anum_pg_proc_proorderkeyarg - 1] = Int16GetDatum(-1);
 	values[Anum_pg_proc_pronargdefaults - 1] = UInt16GetDatum(list_length(parameterDefaults));
 	values[Anum_pg_proc_prorettype - 1] = ObjectIdGetDatum(returnType);
 	values[Anum_pg_proc_proargtypes - 1] = PointerGetDatum(parameterTypes);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index e8ea59e34a..13fe15f7cb 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2216,7 +2216,7 @@ _copyPathKey(const PathKey *from)
 	COPY_SCALAR_FIELD(pk_opfamily);
 	COPY_SCALAR_FIELD(pk_strategy);
 	COPY_SCALAR_FIELD(pk_nulls_first);
-
+	COPY_NODE_FIELD(pk_superkey);
 	return newnode;
 }
 
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3bb91c9595..69ea95d1f6 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -824,6 +824,7 @@ _equalPathKey(const PathKey *a, const PathKey *b)
 	COMPARE_SCALAR_FIELD(pk_opfamily);
 	COMPARE_SCALAR_FIELD(pk_strategy);
 	COMPARE_SCALAR_FIELD(pk_nulls_first);
+	COMPARE_NODE_FIELD(pk_superkey);
 
 	return true;
 }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 69731ccdea..54a69272f2 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2490,6 +2490,7 @@ _outPathKey(StringInfo str, const PathKey *node)
 	WRITE_OID_FIELD(pk_opfamily);
 	WRITE_INT_FIELD(pk_strategy);
 	WRITE_BOOL_FIELD(pk_nulls_first);
+	WRITE_NODE_FIELD(pk_superkey);
 }
 
 static void
diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README
index 9c852a15ef..bfc8f3d523 100644
--- a/src/backend/optimizer/README
+++ b/src/backend/optimizer/README
@@ -553,6 +553,7 @@ the result.  Each PathKey contains these fields:
 	* a btree opfamily OID (must match one of those in the EC)
 	* a sort direction (ascending or descending)
 	* a nulls-first-or-last flag
+	* an optional link to a "super" pathkey.
 
 The EquivalenceClass represents the value being sorted on.  Since the
 various members of an EquivalenceClass are known equal according to the
@@ -667,6 +668,16 @@ if a path was sorted by {a.x} below an outer join, we'll re-sort if that
 sort ordering was important; and so using the same PathKey for both sort
 orderings doesn't create any real problem.
 
+Pathkeys may also contain an optional "super key". The super key is a link
+to another pathkey to which this pathkey's order is derived from.  For
+example a pathkey with the expression "date_trunc('hour', timestamp)"
+could contain a super key containing the expression "timestamp".  This is
+because this particular function truncates away precision, meaning any
+Path which is ordered by "timestamp" is also ordered by
+date_trunc('hour', timestamp).  When checking if a set of path keys is
+contained in another set we make use of any super keys to increase the
+likelihood of a match.  For this to hold true the first parameter of the
+function must be constant.
 
 Order of processing for EquivalenceClasses and PathKeys
 -------------------------------------------------------
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index ec66cb9c3c..d260c5422b 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -25,10 +25,18 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/tlist.h"
+#include "parser/parse_oper.h" /* XXX shouldn't be included here */
 #include "utils/lsyscache.h"
 
 
 static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys);
+static PathKey *make_pathkey_from_sortop(PlannerInfo *root,
+						 Expr *expr,
+						 Relids nullable_relids,
+						 Oid ordering_op,
+						 bool nulls_first,
+						 Index sortref,
+						 bool create_it);
 static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey);
 
 
@@ -146,6 +154,54 @@ pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys)
 	return false;
 }
 
+/*
+ * extract_orderkey_expr
+ *		Analyze 'expr' and attempt to determine if the expr has a suitable
+ *		order key. If it has return it, else return NULL.
+ *
+ * Note: We require that all non-orderkey parameters are Consts.
+ */
+static Expr *
+extract_orderkey_expr(const Expr *expr)
+{
+	if (IsA(expr, FuncExpr))
+	{
+		FuncExpr   *func = (FuncExpr *)expr;
+		int16		orderkeyarg = get_func_orderkeyarg(func->funcid);
+
+		if (orderkeyarg >= 0)
+		{
+			ListCell   *lc;
+			int16		argnum;
+			Expr	   *orderkeyexpr;
+
+			Assert(orderkeyarg < list_length(func->args));
+
+			/*
+			 * Process each argument checking to ensure all arguments apart
+			 * from the order key arg are Const.  Along the way we can
+			 * collect the actual order key arg to save from having to do a
+			 * list_nth on the arg list afterwards.
+			 */
+			argnum = 0;
+			foreach(lc, func->args)
+			{
+				Expr   *argexpr = lfirst(lc);
+
+				if (argnum == orderkeyarg)
+					orderkeyexpr = argexpr;
+				else if (!IsA(argexpr, Const))
+					return NULL;
+				argnum++;
+			}
+			
+			return orderkeyexpr;
+		}
+	}
+
+	return NULL;
+}
+
 /*
  * make_pathkey_from_sortinfo
  *	  Given an expression and sort-order information, create a PathKey.
@@ -179,10 +235,12 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 						   Relids rel,
 						   bool create_it)
 {
+	PathKey	   *pathkey;
 	int16		strategy;
 	Oid			equality_op;
 	List	   *opfamilies;
 	EquivalenceClass *eclass;
+	Expr	   *orderkeyexpr;
 
 	strategy = reverse_sort ? BTGreaterStrategyNumber : BTLessStrategyNumber;
 
@@ -214,8 +272,39 @@ make_pathkey_from_sortinfo(PlannerInfo *root,
 		return NULL;
 
 	/* And finally we can find or create a PathKey node */
-	return make_canonical_pathkey(root, eclass, opfamily,
-								  strategy, nulls_first);
+	pathkey = make_canonical_pathkey(root, eclass, opfamily,
+									 strategy, nulls_first);
+
+	/* Determine if 'expr' contains any orderkeys */
+	orderkeyexpr = extract_orderkey_expr(expr);
+
+	if (orderkeyexpr != NULL)
+	{
+		Oid			sortop;
+		Oid			eqop;
+		bool		hashable;
+
+		if (pathkey->pk_strategy == BTLessStrategyNumber)
+			get_sort_group_operators(exprType((Node *) orderkeyexpr), /* XXX this is not the right function to call */
+									 true, true, false,
+									 &sortop, &eqop, NULL,
+									 &hashable);
+		else
+			get_sort_group_operators(exprType((Node *) orderkeyexpr),
+									 false, true, true,
+									 NULL, &eqop, &sortop,
+									 &hashable);
+
+		pathkey->pk_superkey = make_pathkey_from_sortop(root,
+														orderkeyexpr,
+														nullable_relids,
+														sortop,
+														nulls_first,
+														sortref,
+														create_it);
+	}
+
+	return pathkey;
 }
 
 /*
@@ -311,20 +400,107 @@ compare_pathkeys(List *keys1, List *keys2)
 /*
  * pathkeys_contained_in
  *	  Common special case of compare_pathkeys: we just want to know
- *	  if keys2 are at least as well sorted as keys1.
+ *	  if keys2 are at least as well sorted as keys1. keys1 can exploit any
+ *	  super keys to determine if the order matches.
  */
 bool
 pathkeys_contained_in(List *keys1, List *keys2)
 {
-	switch (compare_pathkeys(keys1, keys2))
+	ListCell   *key1,
+			   *key2;
+
+	/*
+	 * Fall out quickly if we are passed two identical lists.  This mostly
+	 * catches the case where both are NIL, but that's common enough to
+	 * warrant the test.
+	 */
+	if (keys1 == keys2)
+		return true;
+
+	key1 = list_head(keys1);
+
+	foreach(key2, keys2)
 	{
-		case PATHKEYS_EQUAL:
-		case PATHKEYS_BETTER2:
-			return true;
-		default:
-			break;
+		PathKey    *pathkey1;
+		PathKey    *pathkey2 = (PathKey *) lfirst(key2);
+		bool		first = true;
+
+		for (;;)
+		{
+			if (key1 == NULL)
+				goto out;
+
+			pathkey1 = (PathKey *) lfirst(key1);
+
+			if (pathkey1 != pathkey2)
+			{
+				bool		found = false;
+
+				/*
+				 * No match on the main key... see if any super keys exist
+				 * which do match.
+				 */
+
+				pathkey1 = pathkey1->pk_superkey;
+				while (pathkey1 != NULL)
+				{
+					if (pathkey1 == pathkey2)
+					{
+						found = true;
+						break;
+					}
+					pathkey1 = pathkey1->pk_superkey;
+				}
+
+				if (found)
+				{
+					/*
+					 * When we find a matching super key we must try to match
+					 * the next pathkey1 to the same pathkey2. There may be
+					 * multiple pathkey1s relying on this pathkey2.  If we
+					 * don't find a subsequent match then we can just try to
+					 * match to the next pathkey2.
+					 */
+					key1 = lnext(key1);
+					first = false;
+					continue;
+				}
+				else if (first)
+				{
+					/*
+					 * Lack of match is only a problem when its the first
+					 * attempt to match fails
+					 */
+					return false;
+				}
+				else
+				{
+					/*
+					 * Not found, but we've already matched to this key1, so
+					 * skip to next key2.
+					 */
+					break;
+				}
+			}
+			else
+			{
+				key1 = lnext(key1);
+				/* main key matched, just skip to next key2. */
+				break;
+			}
+		}
 	}
-	return false;
+
+out:
+	/*
+	 * If we reached the end of only one list, the other is longer and
+	 * therefore not a subset.
+	 */
+	if (key1 != NULL)
+		return false;	/* key1 is longer */
+	if (key2 != NULL)
+		return true;	/* key2 is longer */
+	return true;
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ae46b0140e..015ecc2244 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -5734,6 +5734,7 @@ prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
 			 * WindowFunc in a sort expression, treat it as a variable.
 			 */
 			Expr	   *sortexpr = NULL;
+			Oid			em_datatype = InvalidOid;
 
 			foreach(j, ec->ec_members)
 			{
@@ -5758,6 +5759,7 @@ prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
 					continue;
 
 				sortexpr = em->em_expr;
+				em_datatype = em->em_datatype;
 				exprvars = pull_var_clause((Node *) sortexpr,
 										   PVC_INCLUDE_AGGREGATES |
 										   PVC_INCLUDE_WINDOWFUNCS |
@@ -5775,7 +5777,21 @@ prepare_sort_from_pathkeys(Plan *lefttree, List *pathkeys,
 				}
 			}
 			if (!j)
-				elog(ERROR, "could not find pathkey item to sort");
+			{
+				/*
+				 * Hard error if we were unable to find an EquivalenceMember
+				 * in order to determine the data type of the sort key
+				 */
+				if (sortexpr == NULL)
+					elog(ERROR, "could not find pathkey item to sort");
+
+				/*
+				 * Otherwise just use the datatype from the EquivalenceMember
+				 * and we'll add a new target list item for the sortexpr
+				 * below.
+				 */
+				pk_datatype = em_datatype;
+			}
 
 			/*
 			 * Do we need to insert a Result node?
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 892ddc0d48..1ce373fdd4 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1440,6 +1440,24 @@ get_func_nargs(Oid funcid)
 	ReleaseSysCache(tp);
 	return result;
 }
+/*
+ * get_func_orderkeyarg
+ *	Given procedure id, return the 0-based order key arg number.
+ */
+int16
+get_func_orderkeyarg(Oid funcid)
+{
+	HeapTuple	tp;
+	int16		result;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	result = ((Form_pg_proc) GETSTRUCT(tp))->proorderkeyarg;
+	ReleaseSysCache(tp);
+	return result;
+}
 
 /*
  * get_func_signature
diff --git a/src/include/catalog/pg_class.dat b/src/include/catalog/pg_class.dat
index 9fffdef379..48ab7b2ec8 100644
--- a/src/include/catalog/pg_class.dat
+++ b/src/include/catalog/pg_class.dat
@@ -47,7 +47,7 @@
   reloftype => '0', relowner => 'PGUID', relam => '0', relfilenode => '0',
   reltablespace => '0', relpages => '0', reltuples => '0', relallvisible => '0',
   reltoastrelid => '0', relhasindex => 'f', relisshared => 'f',
-  relpersistence => 'p', relkind => 'r', relnatts => '28', relchecks => '0',
+  relpersistence => 'p', relkind => 'r', relnatts => '29', relchecks => '0',
   relhasoids => 't', relhasrules => 'f', relhastriggers => 'f',
   relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f',
   relispopulated => 't', relreplident => 'n', relispartition => 'f',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4026018ba9..8c8dbf9aad 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2198,7 +2198,7 @@
   proargtypes => 'text interval', prosrc => 'interval_part' },
 { oid => '1174', descr => 'convert date to timestamp with time zone',
   proname => 'timestamptz', provolatile => 's', prorettype => 'timestamptz',
-  proargtypes => 'date', prosrc => 'date_timestamptz' },
+  proargtypes => 'date', prosrc => 'date_timestamptz', proorderkeyarg => 0 },
 { oid => '2711',
   descr => 'promote groups of 24 hours to numbers of days and promote groups of 30 days to numbers of months',
   proname => 'justify_interval', prorettype => 'interval',
@@ -2279,7 +2279,8 @@
 { oid => '1217',
   descr => 'truncate timestamp with time zone to specified units',
   proname => 'date_trunc', provolatile => 's', prorettype => 'timestamptz',
-  proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc' },
+  proargtypes => 'text timestamptz', prosrc => 'timestamptz_trunc',
+  proorderkeyarg => 1 },
 { oid => '1218', descr => 'truncate interval to specified units',
   proname => 'date_trunc', prorettype => 'interval',
   proargtypes => 'text interval', prosrc => 'interval_trunc' },
@@ -5470,13 +5471,14 @@
   proargtypes => 'timestamptz', prosrc => 'timestamptz_time' },
 { oid => '2020', descr => 'truncate timestamp to specified units',
   proname => 'date_trunc', prorettype => 'timestamp',
-  proargtypes => 'text timestamp', prosrc => 'timestamp_trunc' },
+  proargtypes => 'text timestamp', prosrc => 'timestamp_trunc',
+  proorderkeyarg => 1 },
 { oid => '2021', descr => 'extract field from timestamp',
   proname => 'date_part', prorettype => 'float8',
   proargtypes => 'text timestamp', prosrc => 'timestamp_part' },
 { oid => '2024', descr => 'convert date to timestamp',
   proname => 'timestamp', prorettype => 'timestamp', proargtypes => 'date',
-  prosrc => 'date_timestamp' },
+  prosrc => 'date_timestamp', proorderkeyarg => 0 },
 { oid => '2025', descr => 'convert date and time to timestamp',
   proname => 'timestamp', prorettype => 'timestamp', proargtypes => 'date time',
   prosrc => 'datetime_timestamp' },
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index a34b2596fa..718a21a003 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -79,6 +79,12 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce
 	/* Note: need not be given in pg_proc.dat; genbki.pl will compute it */
 	int16		pronargs;
 
+	/*
+	 * 0-based argument number of the argument that  defines the sort order
+	 * of the return value, or -1 when the function does not support this.
+	 */
+	int16		proorderkeyarg BKI_DEFAULT(-1);
+
 	/* number of arguments with defaults */
 	int16		pronargdefaults BKI_DEFAULT(0);
 
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 88d37236f7..c5e27784e1 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -977,6 +977,10 @@ typedef struct EquivalenceMember
  * equivalent and closely-related orderings. (See optimizer/README for more
  * information.)
  *
+ * PathKeys may also have a "super key". If present describes that the order
+ * described by the key can be satisfied by a path which is ordered by its
+ * 'pk_superkey'.  A super key may in turn have its own super key defined.
+ *
  * Note: pk_strategy is either BTLessStrategyNumber (for ASC) or
  * BTGreaterStrategyNumber (for DESC).  We assume that all ordering-capable
  * index types will use btree-compatible strategy numbers.
@@ -989,6 +993,8 @@ typedef struct PathKey
 	Oid			pk_opfamily;	/* btree opfamily defining the ordering */
 	int			pk_strategy;	/* sort direction (ASC or DESC) */
 	bool		pk_nulls_first; /* do NULLs come before normal values? */
+	struct PathKey *pk_superkey;	/* Link to path key which induces this
+									 * pathkey. */
 } PathKey;
 
 
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index ff1705ad2b..9096442b41 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -111,6 +111,7 @@ extern char *get_func_name(Oid funcid);
 extern Oid	get_func_namespace(Oid funcid);
 extern Oid	get_func_rettype(Oid funcid);
 extern int	get_func_nargs(Oid funcid);
+extern int16 get_func_orderkeyarg(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
 extern Oid	get_func_variadictype(Oid funcid);
 extern bool get_func_retset(Oid funcid);
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index ca27346f18..8150678658 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -1404,3 +1404,37 @@ insert into covidxpart values (4, 1);
 insert into covidxpart values (4, 1);
 ERROR:  duplicate key value violates unique constraint "covidxpart4_a_b_idx"
 DETAIL:  Key (a)=(4) already exists.
+-- Test super path keys
+create table tstbl (ts timestamp, a int);
+create index on tstbl (ts, a);
+set enable_sort = 0;
+explain (costs off) select date_trunc('year', ts), a, count(*) from tstbl group by 1,2 order by 1,2;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ GroupAggregate
+   Group Key: date_trunc('year'::text, ts), a
+   ->  Index Only Scan using tstbl_ts_a_idx on tstbl
+(3 rows)
+
+-- Ensure the we don't use the index to provide pre-sorted input for a
+-- GroupAggregate when the order key expr function has non-constant
+-- non-orderkey arguments.
+explain (costs off) select date_trunc(case random() > 0.5 when true then 'year' else 'month' end, ts), a, count(*) from tstbl group by 1,2 order by 1,2;
+                                                                  QUERY PLAN                                                                  
+----------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: (date_trunc(CASE (random() > '0.5'::double precision) WHEN CASE_TEST_EXPR THEN 'year'::text ELSE 'month'::text END, ts)), a
+   ->  HashAggregate
+         Group Key: date_trunc(CASE (random() > '0.5'::double precision) WHEN CASE_TEST_EXPR THEN 'year'::text ELSE 'month'::text END, ts), a
+         ->  Index Only Scan using tstbl_ts_a_idx on tstbl
+(5 rows)
+
+-- Test a more complex case where the superkey can be matched to multiple pathkeys
+explain (costs off) select date_trunc('year', ts), date_trunc('month', ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ GroupAggregate
+   Group Key: date_trunc('year'::text, ts), date_trunc('month'::text, ts), a
+   ->  Index Only Scan using tstbl_ts_a_idx on tstbl
+(3 rows)
+
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index 400b7eb7ba..9d24e74a7d 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -753,3 +753,18 @@ create unique index on covidxpart4 (a);
 alter table covidxpart attach partition covidxpart4 for values in (4);
 insert into covidxpart values (4, 1);
 insert into covidxpart values (4, 1);
+
+-- Test super path keys
+create table tstbl (ts timestamp, a int);
+create index on tstbl (ts, a);
+set enable_sort = 0;
+
+explain (costs off) select date_trunc('year', ts), a, count(*) from tstbl group by 1,2 order by 1,2;
+
+-- Ensure the we don't use the index to provide pre-sorted input for a
+-- GroupAggregate when the order key expr function has non-constant
+-- non-orderkey arguments.
+explain (costs off) select date_trunc(case random() > 0.5 when true then 'year' else 'month' end, ts), a, count(*) from tstbl group by 1,2 order by 1,2;
+
+-- Test a more complex case where the superkey can be matched to multiple pathkeys
+explain (costs off) select date_trunc('year', ts), date_trunc('month', ts), a, count(*) from tstbl group by 1,2,3 order by 1,2,3;
-- 
2.16.2.windows.1

#7Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#6)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 10/31/2018 04:32 AM, David Rowley wrote:

On 31 October 2018 at 14:23, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

The other thing likely affecting this is locale / collation. Probably
not for date_trunc, but certainly for things like substr()/trim(),
mentioned by Simon upthread.

In some languages the rules are pretty complex, and there's no chance
it'll survive arbitrary substr() applied to the string. For example, in
Czech we mostly sort character-by-character, but "ch" is an exception
sorted in between "h" and "i".

So essentially "hhhh <= hchh <= hihh". Obviously, substr($1,0,3) cuts
the "ch" in half, changing the ordering:

create table x (y text collate "cs_CZ");
insert into x values ('hhhh'), ('hchh'), ('hihh');

test=# select y from x order by 1;
y
------
hhhh
hchh
hihh
(3 rows)

test=# select substr(y,0,3) from x order by 1;
substr
--------
hc
hh
hi
(3 rows)

I'm preeeeeeetty sure other languages have even funnier rules ...

That's pretty interesting, but as mentioned in my initial email...
More careful thought would be needed for other numerical types and
text types, I imagine, though I've not thought much about that.

I don't really think trim() or substr() would ever work for the reason
that they don't always operate on a prefix of the string. What you've
mentioned seems to rule out LEFT().

Sure, but it wasn't very clear to me what the "more careful thought"
would mean.

I think a direct consequence of Tom's point about opclasses is that
storing this information in pg_proc is not going to fly - you would need
a separate catalog for that, to allow mapping the function to multiple
opclasses (possibly with different features?). But OK, that's doable.

But what if you also need to do that for collations? Firstly, it would
it add another degree of freedom, essentially making it (proc, opclass,
collation, ... metadata ...) so there would be many such combinations. I
can't really imagine defining that manually, but maybe it can be
simplified. But more importantly - the list of collations is kinda
dynamic, and AFAIK the collation rules may change depending on the
glibc/icu versions. So, that seems pretty tricky. It's certainly not a
just a SMOP.

I'm mildly suspicious of this data set, because it's essentially
perfectly sorted/deterministic, and the Sort node won't have to do
anything. So perhaps it just works by chance.

Consider this instead:

create table dt (ts timestamp, x text);

insert into dt select * from (select d, (case when random() < 0.5 then
'month' else 'hour' end) from generate_series('2018-01-01'::timestamp,
'2018-12-31'::timestamp, '1 hour'::interval) d) as foo order by random();

[...]

2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-01 00:00:00
2018-01-02 13:00:00
... kaboooooom! ...

Yeah. This is an issue. Thanks for the test case. However, I
acknowledged that in my reply to Tom. I did overlook it, which was
completely down to lack of imagination on my part. I'd never
considered using date_trunc() without a const 1st argument before. It
seems simple enough to disable the optimisation in that case. I've
attached a patch which does that. Maybe that'll help us look beyond
this and focus on any other reasons why this is not possible.

Ah, sorry - I've missed this bit in your response. In my defense, it's
been quite late over here, and my caffeine level got a tad too low.

Anyway, the question is how strong would the requirement need to be, and
how would that affect applicability of this optimization in other cases.
I agree it's probably not an issue for date_trunc() - I don't recall
ever using it with non-constant first parameter. I wonder if there are
other functions where that's not the case - although, that's not really
an argument against this optimization.

It's also true that this diminishes the usefulness of the idea, but
part of the reason I've posting the idea so early after having thought
about it is precisely to see if this is going to float or sink.

Sure. And pgsql-hackers are very good in sinking ideas ;-)

Maybe we'll decide the scope of usefulness is so small that it's not
worth it, or that each function has such different requirements that
we can't reasonably make it work by adding a few columns to pg_proc.
I'm personally more interested in the cases that can work. I
understand there is no shortage of cases where it can't.

I think it can't be made just by adding a couple of columns to pg_proc,
as explained above. A separate catalog mapping procs to opclasses (and
maybe collations) may be needed. For cases where it depends on the
actual parameter values (like substr/trim/ltrim) an extra function might
be needed (something like the selectivity functions for data types).

Giving that we require const arguments away from the orderkey, perhaps
it could be made to work for simple arithmetic OpExprs. I'm not sure
if the use cases are actually there for that sort of thing and I've
seen WHERE indexcol+0 = <n> used many times to disable the use of
indexes, so making pathkeys see through those might be more annoying
than useful... But it's a thought...

Well, that really depends on the definition of the "+" operator, which
is pretty much just a function. So I don't see why would that simplify
the situation?

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#8Robert Haas
robertmhaas@gmail.com
In reply to: Tomas Vondra (#7)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On Wed, Oct 31, 2018 at 9:19 AM Tomas Vondra
<tomas.vondra@2ndquadrant.com> wrote:

I think it can't be made just by adding a couple of columns to pg_proc,
as explained above. A separate catalog mapping procs to opclasses (and
maybe collations) may be needed. For cases where it depends on the
actual parameter values (like substr/trim/ltrim) an extra function might
be needed (something like the selectivity functions for data types).

This kinda reminds me of commit
8f9fe6edce358f7904e0db119416b4d1080a83aa. We needed a way to provide
the planner with knowledge about the behavior of specific functions.
In that case, the specific need was to be able to tell the planner
that a certain function call could be omitted or strength-reduced, and
we did that by having the planner call a function that encoded the
necessary knowledge. Here, we want to evaluate a function call and
see whether it is order preserving, which could depend on a whole
bunch of stuff that isn't easily parameterized by catalog entries, but
could be figured out by a C function written for that purpose. I'm
not really sure how that would work in this case, or whether it's a
good idea, but I thought I'd mention it just in case it's helpful.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#9Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#8)
Re: Super PathKeys (Allowing sort order through precision loss functions)

Robert Haas <robertmhaas@gmail.com> writes:

This kinda reminds me of commit
8f9fe6edce358f7904e0db119416b4d1080a83aa. We needed a way to provide
the planner with knowledge about the behavior of specific functions.
In that case, the specific need was to be able to tell the planner
that a certain function call could be omitted or strength-reduced, and
we did that by having the planner call a function that encoded the
necessary knowledge. Here, we want to evaluate a function call and
see whether it is order preserving, which could depend on a whole
bunch of stuff that isn't easily parameterized by catalog entries, but
could be figured out by a C function written for that purpose.

+1. If we're otherwise going to need multiple pg_proc columns, this
is a better answer just from the standpoint of avoiding pg_proc bloat:
we'd only need to add one OID column. And I concur with your point
that we're going to have a really hard time parameterizing the mechanism
adequately if there isn't dedicated per-function code.

regards, tom lane

#10David Rowley
david.rowley@2ndquadrant.com
In reply to: Robert Haas (#8)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 1 November 2018 at 05:40, Robert Haas <robertmhaas@gmail.com> wrote:

This kinda reminds me of commit
8f9fe6edce358f7904e0db119416b4d1080a83aa. We needed a way to provide
the planner with knowledge about the behavior of specific functions.
In that case, the specific need was to be able to tell the planner
that a certain function call could be omitted or strength-reduced, and
we did that by having the planner call a function that encoded the
necessary knowledge. Here, we want to evaluate a function call and
see whether it is order preserving, which could depend on a whole
bunch of stuff that isn't easily parameterized by catalog entries, but
could be figured out by a C function written for that purpose. I'm
not really sure how that would work in this case, or whether it's a
good idea, but I thought I'd mention it just in case it's helpful.

Agreed. That's a good idea. Thanks.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#11Nasby, Jim
nasbyj@amazon.com
In reply to: Simon Riggs (#2)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On Oct 30, 2018, at 9:08 AM, Simon Riggs <simon@2ndquadrant.com> wrote:

On Tue, 30 Oct 2018 at 07:58, David Rowley <david.rowley@2ndquadrant.com> wrote:

I've started working on something I've ended up calling "Super
PathKeys". The idea here is to increase the likelihood of a Path with
PathKeys being used for a purpose that requires a less strict sort
order due to ordering being required from the return value of some
precision loss function.

Anything left anchored would benefit, so SUBSTR(), TRIM() etc

Main use for this would be where the partition condition is a function, so we can still order by partitions easily.

This would also be very helpful in many BI cases; it’s very common to aggregate based on year, year/month, year/quarter, etc.

The other thing that would be extremely useful would be pushing predicats through this, so you could do things like

WHERE date_trunc(‘year’, timestamp_field) = 2018

#12Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#10)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 10/31/2018 10:07 PM, David Rowley wrote:

On 1 November 2018 at 05:40, Robert Haas <robertmhaas@gmail.com> wrote:

This kinda reminds me of commit
8f9fe6edce358f7904e0db119416b4d1080a83aa. We needed a way to provide
the planner with knowledge about the behavior of specific functions.
In that case, the specific need was to be able to tell the planner
that a certain function call could be omitted or strength-reduced, and
we did that by having the planner call a function that encoded the
necessary knowledge. Here, we want to evaluate a function call and
see whether it is order preserving, which could depend on a whole
bunch of stuff that isn't easily parameterized by catalog entries, but
could be figured out by a C function written for that purpose. I'm
not really sure how that would work in this case, or whether it's a
good idea, but I thought I'd mention it just in case it's helpful.

Agreed. That's a good idea. Thanks.

FWIW this is mostly what I had in mind when referring to the selectivity
estimation functions for operators, although I now realize it might not
have been explained very clearly.

Anyway, I agree this seems like a much better way than trying to store
all the potentially relevant meta-data in catalogs.

I still have trouble imagining what exactly would the function do to
determine if the optimization can be applied to substr() and similar
collation-dependent cases.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#13David Rowley
david.rowley@2ndquadrant.com
In reply to: Tomas Vondra (#12)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 1 November 2018 at 12:11, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

I still have trouble imagining what exactly would the function do to
determine if the optimization can be applied to substr() and similar
collation-dependent cases.

I guess the function would have to check for a Const offset of 0, and
a collection, perhaps of "C" for the 1st arg. In any case, I wouldn't
want this idea to be hung up on the fact we can't determine how to
make substr() work correctly with it.

I'm most interested in date_trunc() and friends. A first cut
implementation would not have to implement functions for everything
that's possible to implement.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#14Andres Freund
andres@anarazel.de
In reply to: David Rowley (#13)
Re: Super PathKeys (Allowing sort order through precision loss functions)

Hi,

On 2018-11-01 12:19:32 +1300, David Rowley wrote:

On 1 November 2018 at 12:11, Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote:

I still have trouble imagining what exactly would the function do to
determine if the optimization can be applied to substr() and similar
collation-dependent cases.

I guess the function would have to check for a Const offset of 0, and
a collection, perhaps of "C" for the 1st arg. In any case, I wouldn't
want this idea to be hung up on the fact we can't determine how to
make substr() work correctly with it.

I'm most interested in date_trunc() and friends. A first cut
implementation would not have to implement functions for everything
that's possible to implement.

FWIW, I kind of wonder if we built proper infrastructure to allow to
make such inferrences from function calls, whether it could also be made
to support the transformation of LIKEs into indexable <= >= clauses.

Greetings,

Andres Freund

#15David Rowley
david.rowley@2ndquadrant.com
In reply to: Andres Freund (#14)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 1 November 2018 at 12:24, Andres Freund <andres@anarazel.de> wrote:

FWIW, I kind of wonder if we built proper infrastructure to allow to
make such inferrences from function calls, whether it could also be made
to support the transformation of LIKEs into indexable <= >= clauses.

Perhaps, but I doubt it would be the same function to do both. Surely
I need something that accepts details about the function call as
arguments and returns an Expr * of the argument that we can derive the
order of the return value from, or NULL. I think the transformation
you need might be more along the lines of returning a List * of quals
that can substitute an OpExpr containing a function call. I'm not that
clear on how we'd know the new quals were better than the existing
ones. For example extract('year', dt) = 2018 could be transformed to
dt >= '2018-01-01' AND dt < '2019-01-01', but how would we know that
was better. There might be an index on extract('year', dt).

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#16Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: David Rowley (#15)
Re: Super PathKeys (Allowing sort order through precision loss functions)

On 11/01/2018 02:40 AM, David Rowley wrote:

On 1 November 2018 at 12:24, Andres Freund <andres@anarazel.de> wrote:

FWIW, I kind of wonder if we built proper infrastructure to allow to
make such inferrences from function calls, whether it could also be made
to support the transformation of LIKEs into indexable <= >= clauses.

Perhaps, but I doubt it would be the same function to do both. Surely
I need something that accepts details about the function call as
arguments and returns an Expr * of the argument that we can derive the
order of the return value from, or NULL. I think the transformation
you need might be more along the lines of returning a List * of quals
that can substitute an OpExpr containing a function call. I'm not that
clear on how we'd know the new quals were better than the existing
ones. For example extract('year', dt) = 2018 could be transformed to
dt >= '2018-01-01' AND dt < '2019-01-01', but how would we know that
was better. There might be an index on extract('year', dt).

IMHO there is only a handful of "useful" transformations of this kind,
depending on the operator class of an index. So when building index
paths and checking which quals may be used as index conditions, we'd do
try transforming incompatible quals and leave the rest up to the
existing create_index_path machinery (which already makes the decision
about which quals to use for index search etc.)

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services