From 0fb9764c651ec2b5ce19147000de18877bf2dd3b Mon Sep 17 00:00:00 2001
From: Floris van Nee <floris.vannee@gmail.com>
Date: Thu, 19 Mar 2020 10:27:47 +0100
Subject: [PATCH 5/5] Support skip scan for non-distinct scans

Adds planner support to choose a skip scan for regular
non-distinct queries like:
SELECT * FROM t1 WHERE b=1 (with index on (a,b))
---
 src/backend/optimizer/path/indxpath.c | 181 +++++++++++++++++++++++++-
 src/backend/optimizer/plan/planner.c  |   2 +-
 src/backend/optimizer/util/pathnode.c |   4 +-
 src/backend/utils/adt/selfuncs.c      | 153 ++++++++++++++++++++--
 src/include/optimizer/pathnode.h      |   3 +-
 5 files changed, 327 insertions(+), 16 deletions(-)

diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 216dd4611e..9bf7bd73f1 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -188,6 +188,17 @@ static Expr *match_clause_to_ordering_op(IndexOptInfo *index,
 static bool ec_member_matches_indexcol(PlannerInfo *root, RelOptInfo *rel,
 									   EquivalenceClass *ec, EquivalenceMember *em,
 									   void *arg);
+static List* add_possible_index_skip_paths(List* result,
+										  PlannerInfo *root,
+										  IndexOptInfo *index,
+										  List *indexclauses,
+										  List *indexorderbys,
+										  List *indexorderbycols,
+										  List *pathkeys,
+										  ScanDirection indexscandir,
+										  bool indexonly,
+										  Relids required_outer,
+										  double loop_count);
 
 
 /*
@@ -816,6 +827,136 @@ get_index_paths(PlannerInfo *root, RelOptInfo *rel,
 	}
 }
 
+/*
+ * Find available index skip paths and add them to the path list
+ */
+static List* add_possible_index_skip_paths(List* result,
+										  PlannerInfo *root,
+										  IndexOptInfo *index,
+										  List *indexclauses,
+										  List *indexorderbys,
+										  List *indexorderbycols,
+										  List *pathkeys,
+										  ScanDirection indexscandir,
+										  bool indexonly,
+										  Relids required_outer,
+										  double loop_count)
+{
+	int			indexcol;
+	bool		eqQualHere;
+	bool		eqQualPrev;
+	bool		eqSoFar;
+	ListCell   *lc;
+
+	/*
+	 * We need to find possible prefixes to use for the skip scan
+	 * Any useful prefix is one just before an index clause, unless
+	 * all clauses so far have been equal.
+	 * For example, on an index (a,b,c), the qual b=1 would
+	 * mean that an interesting skip prefix could be 1.
+	 * For qual a=1 AND b=1, it is not interesting to skip with
+	 * prefix 1, because the value of a is fixed already.
+	 */
+	indexcol = 0;
+	eqQualHere = false;
+	eqQualPrev = false;
+	eqSoFar = true;
+	foreach(lc, indexclauses)
+	{
+		IndexClause *iclause = lfirst_node(IndexClause, lc);
+		ListCell   *lc2;
+
+		if (indexcol != iclause->indexcol)
+		{
+			if (!eqQualHere)
+				eqSoFar = false;
+
+			/* Beginning of a new column's quals */
+			if (!eqQualPrev && !eqSoFar)
+			{
+				/* We have a qual on current column,
+				 * there is no equality qual on the previous column,
+				 * not all of the previous quals are equality so far
+				 * (last one is special case for the first column in the index).
+				 * Optimal conditions to try an index skip path.
+				 */
+				IndexPath *ipath = create_index_path(root, index,
+										  indexclauses,
+										  indexorderbys,
+										  indexorderbycols,
+										  pathkeys,
+										  indexscandir,
+										  indexonly,
+										  required_outer,
+										  loop_count,
+										  false,
+										  iclause->indexcol);
+				result = lappend(result, ipath);
+			}
+
+			eqQualPrev = eqQualHere;
+			eqQualHere = false;
+			indexcol++;
+			/* if the clause is not for this index col, increment until it is */
+			while (indexcol != iclause->indexcol)
+			{
+				eqQualPrev = false;
+				eqSoFar = false;
+				indexcol++;
+			}
+		}
+
+		/* Examine each indexqual associated with this index clause */
+		foreach(lc2, iclause->indexquals)
+		{
+			RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc2);
+			Expr	   *clause = rinfo->clause;
+			Oid			clause_op = InvalidOid;
+			int			op_strategy;
+
+			if (IsA(clause, OpExpr))
+			{
+				OpExpr	   *op = (OpExpr *) clause;
+				clause_op = op->opno;
+			}
+			else if (IsA(clause, RowCompareExpr))
+			{
+				RowCompareExpr *rc = (RowCompareExpr *) clause;
+				clause_op = linitial_oid(rc->opnos);
+			}
+			else if (IsA(clause, ScalarArrayOpExpr))
+			{
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+				clause_op = saop->opno;
+			}
+			else if (IsA(clause, NullTest))
+			{
+				NullTest   *nt = (NullTest *) clause;
+
+				if (nt->nulltesttype == IS_NULL)
+				{
+					/* IS NULL is like = for selectivity purposes */
+					eqQualHere = true;
+				}
+			}
+			else
+				elog(ERROR, "unsupported indexqual type: %d",
+					 (int) nodeTag(clause));
+
+			/* check for equality operator */
+			if (OidIsValid(clause_op))
+			{
+				op_strategy = get_op_opfamily_strategy(clause_op,
+													   index->opfamily[indexcol]);
+				Assert(op_strategy != 0);	/* not a member of opfamily?? */
+				if (op_strategy == BTEqualStrategyNumber)
+					eqQualHere = true;
+			}
+		}
+	}
+	return result;
+}
+
 /*
  * build_index_paths
  *	  Given an index and a set of index clauses for it, construct zero
@@ -1051,9 +1192,25 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 								  index_only_scan,
 								  outer_relids,
 								  loop_count,
-								  false);
+								  false,
+								  0);
 		result = lappend(result, ipath);
 
+		if (can_skip)
+		{
+			result = add_possible_index_skip_paths(result, root, index,
+												   index_clauses,
+												   orderbyclauses,
+												   orderbyclausecols,
+												   useful_pathkeys,
+												   index_is_ordered ?
+												   ForwardScanDirection :
+												   NoMovementScanDirection,
+												   index_only_scan,
+												   outer_relids,
+												   loop_count);
+		}
+
 		/* Consider index skip scan as well */
 		if (root->query_uniquekeys != NULL && can_skip)
 		{
@@ -1100,7 +1257,8 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 									  index_only_scan,
 									  outer_relids,
 									  loop_count,
-									  true);
+									  true,
+									  0);
 
 			/*
 			 * if, after costing the path, we find that it's not worth using
@@ -1133,9 +1291,23 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 									  index_only_scan,
 									  outer_relids,
 									  loop_count,
-									  false);
+									  false,
+									  0);
 			result = lappend(result, ipath);
 
+			if (can_skip)
+			{
+				result = add_possible_index_skip_paths(result, root, index,
+													   index_clauses,
+													   NIL,
+													   NIL,
+													   useful_pathkeys,
+													   BackwardScanDirection,
+													   index_only_scan,
+													   outer_relids,
+													   loop_count);
+			}
+
 			/* Consider index skip scan as well */
 			if (root->query_uniquekeys != NULL && can_skip)
 			{
@@ -1177,7 +1349,8 @@ build_index_paths(PlannerInfo *root, RelOptInfo *rel,
 										  index_only_scan,
 										  outer_relids,
 										  loop_count,
-										  true);
+										  true,
+										  0);
 
 				/*
 				 * if, after costing the path, we find that it's not worth
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 6a5259926d..da2bdcd8d3 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -6356,7 +6356,7 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid)
 	indexScanPath = create_index_path(root, indexInfo,
 									  NIL, NIL, NIL, NIL,
 									  ForwardScanDirection, false,
-									  NULL, 1.0, false);
+									  NULL, 1.0, false, 0);
 
 	return (seqScanAndSortPath.total_cost < indexScanPath->path.total_cost);
 }
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 79af18e7a7..11d3b71ea5 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1019,7 +1019,8 @@ create_index_path(PlannerInfo *root,
 				  bool indexonly,
 				  Relids required_outer,
 				  double loop_count,
-				  bool partial_path)
+				  bool partial_path,
+				  int skip_prefix)
 {
 	IndexPath  *pathnode = makeNode(IndexPath);
 	RelOptInfo *rel = index->rel;
@@ -1039,6 +1040,7 @@ create_index_path(PlannerInfo *root,
 	pathnode->indexorderbys = indexorderbys;
 	pathnode->indexorderbycols = indexorderbycols;
 	pathnode->indexscandir = indexscandir;
+	pathnode->indexskipprefix = skip_prefix;
 
 	cost_index(pathnode, root, loop_count, partial_path);
 
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index be08eb4814..24ea521237 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -210,7 +210,9 @@ static bool get_actual_variable_endpoint(Relation heapRel,
 										 MemoryContext outercontext,
 										 Datum *endpointDatum);
 static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids);
-
+static double estimate_num_groups_internal(PlannerInfo *root, List *groupExprs,
+									double input_rows, double rel_input_rows,
+									List **pgset);
 
 /*
  *		eqsel			- Selectivity of "=" for any data types.
@@ -3356,6 +3358,19 @@ add_unique_group_var(PlannerInfo *root, List *varinfos,
 double
 estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
 					List **pgset)
+{
+	return estimate_num_groups_internal(root, groupExprs, input_rows, -1, pgset);
+}
+
+/*
+ * Same as estimate_num_groups, but with an extra argument to control
+ * the estimation used for the input rows of the relation. If
+ * rel_input_rows < 0, it uses the the original planner estimation for the
+ * individual rels, else if uses the estimation as provided to the function.
+ */
+static double
+estimate_num_groups_internal(PlannerInfo *root, List *groupExprs, double input_rows, double rel_input_rows,
+					List **pgset)
 {
 	List	   *varinfos = NIL;
 	double		srf_multiplier = 1.0;
@@ -3510,6 +3525,12 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
 		int			relvarcount = 0;
 		List	   *newvarinfos = NIL;
 		List	   *relvarinfos = NIL;
+		double this_rel_input_rows;
+
+		if (rel_input_rows < 0.0)
+			this_rel_input_rows = rel->rows;
+		else
+			this_rel_input_rows = rel_input_rows;
 
 		/*
 		 * Split the list of varinfos in two - one for the current rel, one
@@ -3607,7 +3628,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
 			 * guarding against division by zero when reldistinct is zero.
 			 * Also skip this if we know that we are returning all rows.
 			 */
-			if (reldistinct > 0 && rel->rows < rel->tuples)
+			if (reldistinct > 0 && this_rel_input_rows < rel->tuples)
 			{
 				/*
 				 * Given a table containing N rows with n distinct values in a
@@ -3644,7 +3665,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
 				 * works well even when n is small.
 				 */
 				reldistinct *=
-					(1 - pow((rel->tuples - rel->rows) / rel->tuples,
+					(1 - pow((rel->tuples - this_rel_input_rows) / rel->tuples,
 							 rel->tuples / reldistinct));
 			}
 			reldistinct = clamp_row_est(reldistinct);
@@ -6244,8 +6265,10 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	double		numIndexTuples;
 	Cost		descentCost;
 	List	   *indexBoundQuals;
+	List	   *prefixBoundQuals;
 	int			indexcol;
 	bool		eqQualHere;
+	bool		stillEq;
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
@@ -6269,9 +6292,11 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	 * considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
+	prefixBoundQuals = NIL;
 	indexcol = 0;
 	eqQualHere = false;
 	found_saop = false;
+	stillEq = true;
 	found_is_null_op = false;
 	num_sa_scans = 1;
 	foreach(lc, path->indexclauses)
@@ -6283,11 +6308,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		{
 			/* Beginning of a new column's quals */
 			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
+			{
+				stillEq = false;
+				/* done if no '=' qual for indexcol and we're past the skip prefix */
+				if (path->indexskipprefix <= indexcol)
+					break;
+			}
 			eqQualHere = false;
 			indexcol++;
+			while (indexcol != iclause->indexcol && path->indexskipprefix > indexcol)
+				indexcol++;
 			if (indexcol != iclause->indexcol)
-				break;			/* no quals at all for indexcol */
+				break; /* no quals at all for indexcol */
 		}
 
 		/* Examine each indexqual associated with this index clause */
@@ -6319,7 +6351,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 				clause_op = saop->opno;
 				found_saop = true;
 				/* count number of SA scans induced by indexBoundQuals only */
-				if (alength > 1)
+				if (alength > 1 && stillEq)
 					num_sa_scans *= alength;
 			}
 			else if (IsA(clause, NullTest))
@@ -6347,7 +6379,14 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 					eqQualHere = true;
 			}
 
-			indexBoundQuals = lappend(indexBoundQuals, rinfo);
+			/* we keep two lists here, one with all quals up until the prefix
+			 * and one with only the quals until the first inequality.
+			 * we need the list with prefixes later
+			 */
+			if (stillEq)
+				indexBoundQuals = lappend(indexBoundQuals, rinfo);
+			if (path->indexskipprefix > 0)
+				prefixBoundQuals = lappend(prefixBoundQuals, rinfo);
 		}
 	}
 
@@ -6373,7 +6412,10 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 		 * index-bound quals to produce a more accurate idea of the number of
 		 * rows covered by the bound conditions.
 		 */
-		selectivityQuals = add_predicate_to_index_quals(index, indexBoundQuals);
+		if (path->indexskipprefix > 0)
+			selectivityQuals = add_predicate_to_index_quals(index, prefixBoundQuals);
+		else
+			selectivityQuals = add_predicate_to_index_quals(index, indexBoundQuals);
 
 		btreeSelectivity = clauselist_selectivity(root, selectivityQuals,
 												  index->rel->relid,
@@ -6383,7 +6425,7 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 		/*
 		 * As in genericcostestimate(), we have to adjust for any
-		 * ScalarArrayOpExpr quals included in indexBoundQuals, and then round
+		 * ScalarArrayOpExpr quals included in prefixBoundQuals, and then round
 		 * to integer.
 		 */
 		numIndexTuples = rint(numIndexTuples / num_sa_scans);
@@ -6429,6 +6471,99 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 	costs.indexStartupCost += descentCost;
 	costs.indexTotalCost += costs.num_sa_scans * descentCost;
 
+	/*
+	 * Add extra costs for using an index skip scan.
+	 * The index skip scan could have significantly lower cost until now,
+	 * due to the different row estimation used (all the quals up to prefix,
+	 * rather than all the quals up to the first non-equality operator).
+	 * However, there are extra costs incurred for
+	 * a) setting up the scan
+	 * b) doing additional scans from root
+	 * c) small extra cost per tuple comparison
+	 * We add those here
+	 */
+	if (path->indexskipprefix > 0)
+	{
+		List *exprlist = NULL;
+		double numgroups_estimate;
+		int i = 0;
+		ListCell *indexpr_item = list_head(path->indexinfo->indexprs);
+		List	   *selectivityQuals;
+		Selectivity btreeSelectivity;
+		double estimatedIndexTuplesNoPrefix;
+
+		/* some rather arbitrary extra cost for preprocessing structures needed for skip scan */
+		costs.indexStartupCost += 200.0 * cpu_operator_cost;
+		costs.indexTotalCost += 200.0 * cpu_operator_cost;
+
+		/*
+		 * In order to reliably get a cost estimation for the number of scans we have to do from root,
+		 * we need some estimation on the number of distinct prefixes that exist. Therefore, we need
+		 * a different selectivity approximation (this time we do need to use the clauses until the first
+		 * non-equality operator). Using that, we can estimate the number of groups
+		 */
+		for (i = 0; i < path->indexinfo->nkeycolumns && i < path->indexskipprefix; i++)
+		{
+			Expr *expr = NULL;
+			int attr = path->indexinfo->indexkeys[i];
+			if(attr > 0)
+			{
+				TargetEntry *tentry = get_tle_by_resno(path->indexinfo->indextlist, i + 1);
+				Assert(tentry != NULL);
+				expr = tentry->expr;
+			}
+			else if (attr == 0)
+			{
+				/* Expression index */
+				expr = lfirst(indexpr_item);
+				indexpr_item = lnext(path->indexinfo->indexprs, indexpr_item);
+			}
+			else /* attr < 0 */
+			{
+				/* Index on system column is not supported */
+				Assert(false);
+			}
+
+			exprlist = lappend(exprlist, expr);
+		}
+
+		selectivityQuals = add_predicate_to_index_quals(index, indexBoundQuals);
+
+		btreeSelectivity = clauselist_selectivity(root, selectivityQuals,
+												  index->rel->relid,
+												  JOIN_INNER,
+												  NULL);
+		estimatedIndexTuplesNoPrefix = btreeSelectivity * index->rel->tuples;
+
+		/*
+		 * As in genericcostestimate(), we have to adjust for any
+		 * ScalarArrayOpExpr quals included in prefixBoundQuals, and then round
+		 * to integer.
+		 */
+		estimatedIndexTuplesNoPrefix = rint(estimatedIndexTuplesNoPrefix / num_sa_scans);
+
+		numgroups_estimate = estimate_num_groups_internal(
+					root, exprlist, estimatedIndexTuplesNoPrefix,
+					estimatedIndexTuplesNoPrefix, NULL);
+
+		/*
+		 * For each distinct prefix value we add descending cost as.
+		 * This is similar to the startup cost calculation for regular scans.
+		 * We can do at most 2 scans from root per distinct prefix, so multiply by 2.
+		 * Also add some CPU processing cost per page that we need to process, plus
+		 * some additional one-time cost for scanning the leaf page. This is a more
+		 * expensive estimation than the per-page cpu cost for the regular index scan.
+		 * This is intentional, because the index skip scan does more processing on
+		 * the leaf page.
+		 */
+		if (index->tuples > 0)
+			descentCost = ceil(log(index->tuples) / log(2.0)) * cpu_operator_cost * 2;
+		else
+			descentCost = 0;
+		descentCost += (index->tree_height + 1) * 50.0 * cpu_operator_cost * 2 + 200 * cpu_operator_cost;
+		costs.indexTotalCost += costs.num_sa_scans * descentCost * numgroups_estimate;
+	}
+
 	/*
 	 * If we can get an estimate of the first column's ordering correlation C
 	 * from pg_statistic, estimate the index correlation as C for a
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 8ec1780a56..4fa9adbfee 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -49,7 +49,8 @@ extern IndexPath *create_index_path(PlannerInfo *root,
 									bool indexonly,
 									Relids required_outer,
 									double loop_count,
-									bool partial_path);
+									bool partial_path,
+									int skip_prefix);
 extern BitmapHeapPath *create_bitmap_heap_path(PlannerInfo *root,
 											   RelOptInfo *rel,
 											   Path *bitmapqual,
-- 
2.27.0

