Commits

Tom Lane  committed e2c2c2e

Improve planner's handling of duplicated index column expressions.

It's potentially useful for an index to repeat the same indexable column
or expression in multiple index columns, if the columns have different
opclasses. (If they share opclasses too, the duplicate column is pretty
useless, but nonetheless we've allowed such cases since 9.0.) However,
the planner failed to cope with this, because createplan.c was relying on
simple equal() matching to figure out which index column each index qual
is intended for. We do have that information available upstream in
indxpath.c, though, so the fix is to not flatten the multi-level indexquals
list when putting it into an IndexPath. Then we can rely on the sublist
structure to identify target index columns in createplan.c. There's a
similar issue for index ORDER BYs (the KNNGIST feature), so introduce a
multi-level-list representation for that too. This adds a bit more
representational overhead, but we might more or less buy that back by not
having to search for matching index columns anymore in createplan.c;
likewise btcostestimate saves some cycles.

Per bug #6351 from Christian Rudolph. Likely symptoms include the "btree
index keys must be ordered by attribute" failure shown there, as well as
"operator MMMM is not a member of opfamily NNNN".

Although this is a pre-existing problem that can be demonstrated in 9.0 and
9.1, I'm not going to back-patch it, because the API changes in the planner
seem likely to break things such as index plugins. The corner cases where
this matters seem too narrow to justify possibly breaking things in a minor
release.

  • Participants
  • Parent commits d5448c7

Comments (0)

Files changed (12)

File src/backend/optimizer/path/costsize.c

  *	  Determines and returns the cost of scanning a relation using an index.
  *
  * 'index' is the index to be used
- * 'indexQuals' is the list of applicable qual clauses (implicit AND semantics)
- * 'indexOrderBys' is the list of ORDER BY operators for amcanorderbyop indexes
+ * 'indexQuals' is a list of lists of applicable qual clauses (implicit AND
+ *		semantics, one sub-list per index column)
+ * 'indexOrderBys' is a list of lists of lists of ORDER BY expressions for
+ *		amcanorderbyop indexes (lists per pathkey and index column)
  * 'indexonly' is true if it's an index-only scan
  * 'outer_rel' is the outer relation when we are considering using the index
  *		scan as the inside of a nestloop join (hence, some of the indexQuals
  * additional fields of the IndexPath besides startup_cost and total_cost.
  * These fields are needed if the IndexPath is used in a BitmapIndexScan.
  *
- * indexQuals is a list of RestrictInfo nodes, but indexOrderBys is a list of
- * bare expressions.
+ * indexQuals is a list of lists of RestrictInfo nodes, but indexOrderBys
+ * is a list of lists of lists of bare expressions.
  *
  * NOTE: 'indexQuals' must contain only clauses usable as index restrictions.
  * Any additional quals evaluated as qpquals may reduce the number of returned

File src/backend/optimizer/path/indxpath.c

  * Returns a list of sublists of RestrictInfo nodes for clauses that can be
  * used with this index.  Each sublist contains clauses that can be used
  * with one index key (in no particular order); the top list is ordered by
- * index key.  (This is depended on by expand_indexqual_conditions().)
+ * index key.  (This is depended on by expand_indexqual_conditions() and
+ * fix_indexqual_references().)
  *
  * We can use clauses from either the current clauses or outer_clauses lists,
  * but *found_clause is set TRUE only if we used at least one clause from
  * column C, and no clauses use column B.
  *
  * Note: in some circumstances we may find the same RestrictInfos coming
- * from multiple places.  Defend against redundant outputs by using
- * list_append_unique_ptr (pointer equality should be good enough).
+ * from multiple places.  Defend against redundant outputs by keeping a side
+ * list of already-used clauses (pointer equality should be a good enough
+ * check for this).  That also keeps us from matching the same clause to
+ * multiple columns of a badly-defined index, which is unlikely to be helpful
+ * and is likely to give us an inflated idea of the index's selectivity.
  */
 static List *
 group_clauses_by_indexkey(IndexOptInfo *index,
 						  bool *found_clause)
 {
 	List	   *clausegroup_list = NIL;
+	List	   *used_clauses = NIL;
 	bool		found_outer_clause = false;
 	int			indexcol;
 
 			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
 
 			Assert(IsA(rinfo, RestrictInfo));
+			if (list_member_ptr(used_clauses, rinfo))
+				continue;
 			if (match_clause_to_indexcol(index,
 										 indexcol,
 										 rinfo,
 										 outer_relids,
 										 saop_control))
 			{
-				clausegroup = list_append_unique_ptr(clausegroup, rinfo);
+				clausegroup = lappend(clausegroup, rinfo);
+				used_clauses = lappend(used_clauses, rinfo);
 				if (saop_control != SAOP_REQUIRE ||
 					IsA(rinfo->clause, ScalarArrayOpExpr))
 					*found_clause = true;
 			RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
 
 			Assert(IsA(rinfo, RestrictInfo));
+			if (list_member_ptr(used_clauses, rinfo))
+				continue;
 			if (match_clause_to_indexcol(index,
 										 indexcol,
 										 rinfo,
 										 outer_relids,
 										 saop_control))
 			{
-				clausegroup = list_append_unique_ptr(clausegroup, rinfo);
+				clausegroup = lappend(clausegroup, rinfo);
+				used_clauses = lappend(used_clauses, rinfo);
 				found_outer_clause = true;
 			}
 		}
 		clausegroup_list = lappend(clausegroup_list, clausegroup);
 	}
 
+	list_free(used_clauses);
+
 	if (!*found_clause && !found_outer_clause)
 		return NIL;				/* no indexable clauses anywhere */
 
  *	  target index column.	This is sufficient to guarantee that some index
  *	  condition can be constructed from the RowCompareExpr --- whether the
  *	  remaining columns match the index too is considered in
- *	  expand_indexqual_rowcompare().
+ *	  adjust_rowcompare_for_index().
  *
  *	  It is also possible to match ScalarArrayOpExpr clauses to indexes, when
  *	  the clause is of the form "indexkey op ANY (arrayconst)".  Since not
  *		Test whether an index can produce output ordered according to the
  *		given pathkeys using "ordering operators".
  *
- * If it can, return a list of suitable ORDER BY expressions, each of the form
- * "indexedcol operator pseudoconstant".  If not, return NIL.
+ * If it can, return a list of lists of lists of ORDER BY expressions,
+ * each of the form "indexedcol operator pseudoconstant".  If not, return NIL.
+ * (The top list corresponds to pathkeys and the sub-lists to index columns;
+ * see comments for indexorderbys in nodes/relation.h.)
  */
 static List *
 match_index_to_pathkeys(IndexOptInfo *index, List *pathkeys)
 {
-	List	   *orderbyexprs = NIL;
+	List	   *orderbylists = NIL;
 	ListCell   *lc1;
 
 	/* Only indexes with the amcanorderbyop property are interesting here */
 												   pathkey->pk_opfamily);
 				if (expr)
 				{
-					orderbyexprs = lappend(orderbyexprs, expr);
+					/*
+					 * Generate list-of-sublists representation to show which
+					 * index column this expression matches.
+					 */
+					List   *sublist = NIL;
+					int		i;
+
+					for (i = 0; i < indexcol; i++)
+						sublist = lappend(sublist, NIL);
+					sublist = lappend(sublist, list_make1(expr));
+					orderbylists = lappend(orderbylists, sublist);
 					found = true;
 					break;
 				}
 			return NIL;
 	}
 
-	return orderbyexprs;		/* success! */
+	return orderbylists;		/* success! */
 }
 
 /*
  *	  Given a list of lists of RestrictInfos, flatten it to a list
  *	  of RestrictInfos.
  *
- * This is used to flatten out the result of group_clauses_by_indexkey()
- * to produce an indexclauses list.  The original list structure mustn't
- * be altered, but it's OK to share copies of the underlying RestrictInfos.
+ * This is used to flatten out a list of sublists of index clauses (such as
+ * the result of group_clauses_by_indexkey()) into a single list, for use
+ * where we don't care which clause goes with which index column.  The input
+ * list structure mustn't be altered, but it's OK to share copies of the
+ * underlying RestrictInfos.
  */
 List *
 flatten_clausegroups_list(List *clausegroups)
 	return allclauses;
 }
 
+/*
+ * flatten_indexorderbys_list
+ *	  Given a list of lists of lists of ORDER BY expressions, flatten it.
+ *
+ * This is similar to flatten_clausegroups_list, but is used to flatten the
+ * three-list-level result of match_index_to_pathkeys().  We assume the
+ * bottom lists each have zero or one member.
+ */
+List *
+flatten_indexorderbys_list(List *indexorderbys)
+{
+	List	   *allclauses = NIL;
+	ListCell   *lc1;
+
+	foreach(lc1, indexorderbys)
+	{
+		List	   *sublist = (List *) lfirst(lc1);
+		ListCell   *lc2;
+
+		foreach(lc2, sublist)
+		{
+			List	   *subsublist = (List *) lfirst(lc2);
+
+			if (subsublist == NIL)
+				continue;
+			Assert(list_length(subsublist) == 1);
+			allclauses = lappend(allclauses, (Expr *) linitial(subsublist));
+		}
+	}
+	return allclauses;
+}
+
 
 /****************************************************************************
  *				----  ROUTINES TO CHECK OPERANDS  ----
  * converted into boolean equality operators.
  *
  * expand_indexqual_conditions() converts a list of lists of RestrictInfo
- * nodes (with implicit AND semantics across list elements) into
- * a list of clauses that the executor can actually handle.  For operators
+ * nodes (with implicit AND semantics across list elements) into a list of
+ * lists of clauses that the executor can actually handle.  For operators
  * that are members of the index's opfamily this transformation is a no-op,
  * but clauses recognized by match_special_index_operator() or
  * match_boolean_index_clause() must be converted into one or more "regular"
 
 /*
  * expand_indexqual_conditions
- *	  Given a list of sublists of RestrictInfo nodes, produce a flat list
+ *	  Given a list of sublists of RestrictInfo nodes, produce a list of lists
  *	  of index qual clauses.  Standard qual clauses (those in the index's
  *	  opfamily) are passed through unchanged.  Boolean clauses and "special"
  *	  index operators are expanded into clauses that the indexscan machinery
  *	  will know what to do with.  RowCompare clauses are simplified if
  *	  necessary to create a clause that is fully checkable by the index.
  *
- * The input list is ordered by index key, and so the output list is too.
- * (The latter is not depended on by any part of the core planner, I believe,
- * but parts of the executor require it, and so do the amcostestimate
- * functions.)
+ * The input clauses are grouped by index key, and so the output is too.
+ * (This is depended on in various places in both planner and executor.)
  */
 List *
 expand_indexqual_conditions(IndexOptInfo *index, List *clausegroups)
 {
-	List	   *resultquals = NIL;
+	List	   *resultgroups = NIL;
 	ListCell   *lc;
 	int			indexcol;
 
 		List	   *clausegroup = (List *) lfirst(lc);
 		Oid			curFamily = index->opfamily[indexcol];
 		Oid			curCollation = index->indexcollations[indexcol];
+		List	   *newgroup = NIL;
 		ListCell   *lc2;
 
 		foreach(lc2, clausegroup)
 													   index);
 				if (boolqual)
 				{
-					resultquals = lappend(resultquals,
-										  make_simple_restrictinfo(boolqual));
+					newgroup = lappend(newgroup,
+									   make_simple_restrictinfo(boolqual));
 					continue;
 				}
 			}
 			 */
 			if (is_opclause(clause))
 			{
-				resultquals = list_concat(resultquals,
-										  expand_indexqual_opclause(rinfo,
-																	curFamily,
+				newgroup = list_concat(newgroup,
+									   expand_indexqual_opclause(rinfo,
+																 curFamily,
 															  curCollation));
 			}
 			else if (IsA(clause, ScalarArrayOpExpr))
 			{
 				/* no extra work at this time */
-				resultquals = lappend(resultquals, rinfo);
+				newgroup = lappend(newgroup, rinfo);
 			}
 			else if (IsA(clause, RowCompareExpr))
 			{
-				resultquals = lappend(resultquals,
-									  expand_indexqual_rowcompare(rinfo,
-																  index,
-																  indexcol));
+				newgroup = lappend(newgroup,
+								   expand_indexqual_rowcompare(rinfo,
+															   index,
+															   indexcol));
 			}
 			else if (IsA(clause, NullTest))
 			{
 				Assert(index->amsearchnulls);
-				resultquals = lappend(resultquals,
-									  make_simple_restrictinfo(clause));
+				newgroup = lappend(newgroup,
+								   make_simple_restrictinfo(clause));
 			}
 			else
 				elog(ERROR, "unsupported indexqual type: %d",
 					 (int) nodeTag(clause));
 		}
 
+		resultgroups = lappend(resultgroups, newgroup);
+
 		indexcol++;
 	}
 
-	return resultquals;
+	return resultgroups;
 }
 
 /*
  * expand_indexqual_rowcompare --- expand a single indexqual condition
  *		that is a RowCompareExpr
  *
+ * This is a thin wrapper around adjust_rowcompare_for_index; we export the
+ * latter so that createplan.c can use it to re-discover which columns of the
+ * index are used by a row comparison indexqual.
+ */
+static RestrictInfo *
+expand_indexqual_rowcompare(RestrictInfo *rinfo,
+							IndexOptInfo *index,
+							int indexcol)
+{
+	RowCompareExpr *clause = (RowCompareExpr *) rinfo->clause;
+	Expr	   *newclause;
+	List	   *indexcolnos;
+	bool		var_on_left;
+
+	newclause = adjust_rowcompare_for_index(clause,
+											index,
+											indexcol,
+											&indexcolnos,
+											&var_on_left);
+
+	/*
+	 * If we didn't have to change the RowCompareExpr, return the original
+	 * RestrictInfo.
+	 */
+	if (newclause == (Expr *) clause)
+		return rinfo;
+
+	/* Else we need a new RestrictInfo */
+	return make_simple_restrictinfo(newclause);
+}
+
+/*
+ * adjust_rowcompare_for_index --- expand a single indexqual condition
+ *		that is a RowCompareExpr
+ *
  * It's already known that the first column of the row comparison matches
  * the specified column of the index.  We can use additional columns of the
  * row comparison as index qualifications, so long as they match the index
  * even when the original was "<" or ">" --- this is necessary to match all
  * the rows that could match the original.	(We are essentially building a
  * lossy version of the row comparison when we do this.)
+ *
+ * *indexcolnos receives an integer list of the index column numbers (zero
+ * based) used in the resulting expression.  The reason we need to return
+ * that is that if the index is selected for use, createplan.c will need to
+ * call this again to extract that list.  (This is a bit grotty, but row
+ * comparison indexquals aren't used enough to justify finding someplace to
+ * keep the information in the Path representation.)  Since createplan.c
+ * also needs to know which side of the RowCompareExpr is the index side,
+ * we also return *var_on_left_p rather than re-deducing that there.
  */
-static RestrictInfo *
-expand_indexqual_rowcompare(RestrictInfo *rinfo,
+Expr *
+adjust_rowcompare_for_index(RowCompareExpr *clause,
 							IndexOptInfo *index,
-							int indexcol)
+							int indexcol,
+							List **indexcolnos,
+							bool *var_on_left_p)
 {
-	RowCompareExpr *clause = (RowCompareExpr *) rinfo->clause;
 	bool		var_on_left;
 	int			op_strategy;
 	Oid			op_lefttype;
 	Assert(var_on_left ||
 		   match_index_to_operand((Node *) linitial(clause->rargs),
 								  indexcol, index));
+	*var_on_left_p = var_on_left;
+
 	expr_op = linitial_oid(clause->opnos);
 	if (!var_on_left)
 		expr_op = get_commutator(expr_op);
 							   &op_strategy,
 							   &op_lefttype,
 							   &op_righttype);
+
+	/* Initialize returned list of which index columns are used */
+	*indexcolnos = list_make1_int(indexcol);
+
 	/* Build lists of the opfamilies and operator datatypes in case needed */
 	opfamilies = list_make1_oid(index->opfamily[indexcol]);
 	lefttypes = list_make1_oid(op_lefttype);
 			break;				/* no good, volatile comparison value */
 
 		/*
-		 * The Var side can match any column of the index.	If the user does
-		 * something weird like having multiple identical index columns, we
-		 * insist the match be on the first such column, to avoid confusing
-		 * the executor.
+		 * The Var side can match any column of the index.
 		 */
 		for (i = 0; i < index->ncolumns; i++)
 		{
-			if (match_index_to_operand(varop, i, index))
+			if (match_index_to_operand(varop, i, index) &&
+				get_op_opfamily_strategy(expr_op,
+										 index->opfamily[i]) == op_strategy &&
+				IndexCollMatchesExprColl(index->indexcollations[i],
+										 lfirst_oid(collids_cell)))
 				break;
 		}
 		if (i >= index->ncolumns)
 			break;				/* no match found */
 
-		/* Now, do we have the right operator for this column? */
-		if (get_op_opfamily_strategy(expr_op, index->opfamily[i])
-			!= op_strategy)
-			break;
-
-		/* Does collation match? */
-		if (!IndexCollMatchesExprColl(index->indexcollations[i],
-									  lfirst_oid(collids_cell)))
-			break;
+		/* Add column number to returned list */
+		*indexcolnos = lappend_int(*indexcolnos, i);
 
 		/* Add opfamily and datatypes to lists */
 		get_op_opfamily_properties(expr_op, index->opfamily[i], false,
 
 	/* Return clause as-is if it's all usable as index quals */
 	if (matching_cols == list_length(clause->opnos))
-		return rinfo;
+		return (Expr *) clause;
 
 	/*
 	 * We have to generate a subset rowcompare (possibly just one OpExpr). The
 								  matching_cols);
 		rc->rargs = list_truncate((List *) copyObject(clause->rargs),
 								  matching_cols);
-		return make_simple_restrictinfo((Expr *) rc);
+		return (Expr *) rc;
 	}
 	else
 	{
-		Expr	   *opexpr;
-
-		opexpr = make_opclause(linitial_oid(new_ops), BOOLOID, false,
-							   copyObject(linitial(clause->largs)),
-							   copyObject(linitial(clause->rargs)),
-							   InvalidOid,
-							   linitial_oid(clause->inputcollids));
-		return make_simple_restrictinfo(opexpr);
+		return make_opclause(linitial_oid(new_ops), BOOLOID, false,
+							 copyObject(linitial(clause->largs)),
+							 copyObject(linitial(clause->rargs)),
+							 InvalidOid,
+							 linitial_oid(clause->inputcollids));
 	}
 }
 

File src/backend/optimizer/plan/createplan.c

 						 List *indexquals);
 static List *fix_indexorderby_references(PlannerInfo *root, IndexPath *index_path,
 							List *indexorderbys);
-static Node *fix_indexqual_operand(Node *node, IndexOptInfo *index);
+static Node *fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol);
 static List *get_switched_clauses(List *clauses, Relids outerrelids);
 static List *order_qual_clauses(PlannerInfo *root, List *clauses);
 static void copy_path_costsize(Plan *dest, Path *src);
  * qual preprocessing work is the same for both.  Note that the caller tells
  * us which to build --- we don't look at best_path->path.pathtype, because
  * create_bitmap_subplan needs to be able to override the prior decision.
- *
- * The indexquals list of the path contains implicitly-ANDed qual conditions.
- * The list can be empty --- then no index restrictions will be applied during
- * the scan.
  */
 static Scan *
 create_indexscan_plan(PlannerInfo *root,
 					  bool indexonly)
 {
 	Scan	   *scan_plan;
-	List	   *indexquals = best_path->indexquals;
 	List	   *indexorderbys = best_path->indexorderbys;
 	Index		baserelid = best_path->path.parent->relid;
 	Oid			indexoid = best_path->indexinfo->indexoid;
 	List	   *qpqual;
+	List	   *indexquals;
 	List	   *stripped_indexquals;
 	List	   *fixed_indexquals;
 	List	   *fixed_indexorderbys;
 	Assert(best_path->path.parent->rtekind == RTE_RELATION);
 
 	/*
+	 * We need to flatten the indexquals list-of-sublists, since most of the
+	 * processing below doesn't care which index column each qual is
+	 * associated with.
+	 */
+	indexquals = flatten_clausegroups_list(best_path->indexquals);
+
+	/*
 	 * Build "stripped" indexquals structure (no RestrictInfos) to pass to
 	 * executor as indexqualorig
 	 */
 
 	/*
 	 * The executor needs a copy with the indexkey on the left of each clause
-	 * and with index Vars substituted for table ones.
+	 * and with index Vars substituted for table ones.  Here we use the
+	 * unflattened list so we can conveniently tell which index column each
+	 * clause is for.
 	 */
-	fixed_indexquals = fix_indexqual_references(root, best_path, indexquals);
+	fixed_indexquals = fix_indexqual_references(root, best_path,
+												best_path->indexquals);
 
 	/*
 	 * Likewise fix up index attr references in the ORDER BY expressions.
 	 */
-	fixed_indexorderbys = fix_indexorderby_references(root, best_path, indexorderbys);
+	fixed_indexorderbys = fix_indexorderby_references(root, best_path,
+													  indexorderbys);
+
+	/*
+	 * Also produce a flat list to become the indexorderbyorig.
+	 */
+	indexorderbys = flatten_indexorderbys_list(indexorderbys);
 
 	/*
 	 * If this is an innerjoin scan, the indexclauses will contain join
 			clamp_row_est(ipath->indexselectivity * ipath->path.parent->tuples);
 		plan->plan_width = 0;	/* meaningless */
 		*qual = get_actual_clauses(ipath->indexclauses);
-		*indexqual = get_actual_clauses(ipath->indexquals);
+		*indexqual = get_actual_clauses(flatten_clausegroups_list(ipath->indexquals));
 		foreach(l, ipath->indexinfo->indpred)
 		{
 			Expr	   *pred = (Expr *) lfirst(l);
  *	  Adjust indexqual clauses to the form the executor's indexqual
  *	  machinery needs.
  *
- * We have four tasks here:
+ * We have five tasks here:
+ *	* Flatten the list-of-sublists structure of indexquals into a simple list.
  *	* Remove RestrictInfo nodes from the input clauses.
  *	* Replace any outer-relation Var or PHV nodes with nestloop Params.
  *	  (XXX eventually, that responsibility should go elsewhere?)
 {
 	IndexOptInfo *index = index_path->indexinfo;
 	List	   *fixed_indexquals;
-	ListCell   *l;
+	ListCell   *lc1;
+	int			indexcol;
 
 	fixed_indexquals = NIL;
 
-	foreach(l, indexquals)
-	{
-		RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-		Node	   *clause;
-
-		Assert(IsA(rinfo, RestrictInfo));
+	/* clausegroups must correspond to index columns */
+	Assert(list_length(indexquals) <= index->ncolumns);
 
-		/*
-		 * Replace any outer-relation variables with nestloop params.
-		 *
-		 * This also makes a copy of the clause, so it's safe to modify it
-		 * in-place below.
-		 */
-		clause = replace_nestloop_params(root, (Node *) rinfo->clause);
+	indexcol = 0;
+	foreach(lc1, indexquals)
+	{
+		List	   *clausegroup = (List *) lfirst(lc1);
+		ListCell   *lc2;
 
-		if (IsA(clause, OpExpr))
+		foreach(lc2, clausegroup)
 		{
-			OpExpr	   *op = (OpExpr *) clause;
+			RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc2);
+			Node	   *clause;
 
-			if (list_length(op->args) != 2)
-				elog(ERROR, "indexqual clause is not binary opclause");
+			Assert(IsA(rinfo, RestrictInfo));
 
 			/*
-			 * Check to see if the indexkey is on the right; if so, commute
-			 * the clause. The indexkey should be the side that refers to
-			 * (only) the base relation.
+			 * Replace any outer-relation variables with nestloop params.
+			 *
+			 * This also makes a copy of the clause, so it's safe to modify it
+			 * in-place below.
 			 */
-			if (!bms_equal(rinfo->left_relids, index->rel->relids))
-				CommuteOpExpr(op);
+			clause = replace_nestloop_params(root, (Node *) rinfo->clause);
 
-			/*
-			 * Now, determine which index attribute this is and change the
-			 * indexkey operand as needed.
-			 */
-			linitial(op->args) = fix_indexqual_operand(linitial(op->args),
-													   index);
-		}
-		else if (IsA(clause, RowCompareExpr))
-		{
-			RowCompareExpr *rc = (RowCompareExpr *) clause;
-			ListCell   *lc;
+			if (IsA(clause, OpExpr))
+			{
+				OpExpr	   *op = (OpExpr *) clause;
 
-			/*
-			 * Check to see if the indexkey is on the right; if so, commute
-			 * the clause. The indexkey should be the side that refers to
-			 * (only) the base relation.
-			 */
-			if (!bms_overlap(pull_varnos(linitial(rc->largs)),
-							 index->rel->relids))
-				CommuteRowCompareExpr(rc);
+				if (list_length(op->args) != 2)
+					elog(ERROR, "indexqual clause is not binary opclause");
 
-			/*
-			 * For each column in the row comparison, determine which index
-			 * attribute this is and change the indexkey operand as needed.
-			 */
-			foreach(lc, rc->largs)
+				/*
+				 * Check to see if the indexkey is on the right; if so,
+				 * commute the clause. The indexkey should be the side that
+				 * refers to (only) the base relation.
+				 */
+				if (!bms_equal(rinfo->left_relids, index->rel->relids))
+					CommuteOpExpr(op);
+
+				/*
+				 * Now replace the indexkey expression with an index Var.
+				 */
+				linitial(op->args) = fix_indexqual_operand(linitial(op->args),
+														   index,
+														   indexcol);
+			}
+			else if (IsA(clause, RowCompareExpr))
 			{
-				lfirst(lc) = fix_indexqual_operand(lfirst(lc),
-												   index);
+				RowCompareExpr *rc = (RowCompareExpr *) clause;
+				Expr	   *newrc;
+				List	   *indexcolnos;
+				bool		var_on_left;
+				ListCell   *lca,
+						   *lci;
+
+				/*
+				 * Re-discover which index columns are used in the rowcompare.
+				 */
+				newrc = adjust_rowcompare_for_index(rc,
+													index,
+													indexcol,
+													&indexcolnos,
+													&var_on_left);
+
+				/*
+				 * Trouble if adjust_rowcompare_for_index thought the
+				 * RowCompareExpr didn't match the index as-is; the clause
+				 * should have gone through that routine already.
+				 */
+				if (newrc != (Expr *) rc)
+					elog(ERROR, "inconsistent results from adjust_rowcompare_for_index");
+
+				/*
+				 * Check to see if the indexkey is on the right; if so,
+				 * commute the clause.
+				 */
+				if (!var_on_left)
+					CommuteRowCompareExpr(rc);
+
+				/*
+				 * Now replace the indexkey expressions with index Vars.
+				 */
+				Assert(list_length(rc->largs) == list_length(indexcolnos));
+				forboth(lca, rc->largs, lci, indexcolnos)
+				{
+					lfirst(lca) = fix_indexqual_operand(lfirst(lca),
+														index,
+														lfirst_int(lci));
+				}
 			}
-		}
-		else if (IsA(clause, ScalarArrayOpExpr))
-		{
-			ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+			else if (IsA(clause, ScalarArrayOpExpr))
+			{
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
 
-			/* Never need to commute... */
+				/* Never need to commute... */
 
-			/*
-			 * Determine which index attribute this is and change the indexkey
-			 * operand as needed.
-			 */
-			linitial(saop->args) = fix_indexqual_operand(linitial(saop->args),
-														 index);
-		}
-		else if (IsA(clause, NullTest))
-		{
-			NullTest   *nt = (NullTest *) clause;
+				/* Replace the indexkey expression with an index Var. */
+				linitial(saop->args) = fix_indexqual_operand(linitial(saop->args),
+															 index,
+															 indexcol);
+			}
+			else if (IsA(clause, NullTest))
+			{
+				NullTest   *nt = (NullTest *) clause;
+
+				/* Replace the indexkey expression with an index Var. */
+				nt->arg = (Expr *) fix_indexqual_operand((Node *) nt->arg,
+														 index,
+														 indexcol);
+			}
+			else
+				elog(ERROR, "unsupported indexqual type: %d",
+					 (int) nodeTag(clause));
 
-			nt->arg = (Expr *) fix_indexqual_operand((Node *) nt->arg,
-													 index);
+			fixed_indexquals = lappend(fixed_indexquals, clause);
 		}
-		else
-			elog(ERROR, "unsupported indexqual type: %d",
-				 (int) nodeTag(clause));
 
-		fixed_indexquals = lappend(fixed_indexquals, clause);
+		indexcol++;
 	}
 
 	return fixed_indexquals;
  *	  machinery needs.
  *
  * This is a simplified version of fix_indexqual_references.  The input does
- * not have RestrictInfo nodes, and we assume that indxqual.c already
+ * not have RestrictInfo nodes, and we assume that indxpath.c already
  * commuted the clauses to put the index keys on the left.	Also, we don't
  * bother to support any cases except simple OpExprs, since nothing else
  * is allowed for ordering operators.
 {
 	IndexOptInfo *index = index_path->indexinfo;
 	List	   *fixed_indexorderbys;
-	ListCell   *l;
+	ListCell   *lc1;
 
 	fixed_indexorderbys = NIL;
 
-	foreach(l, indexorderbys)
+	foreach(lc1, indexorderbys)
 	{
-		Node	   *clause = (Node *) lfirst(l);
+		List	   *percollists = (List *) lfirst(lc1);
+		ListCell   *lc2;
+		int			indexcol;
 
-		/*
-		 * Replace any outer-relation variables with nestloop params.
-		 *
-		 * This also makes a copy of the clause, so it's safe to modify it
-		 * in-place below.
-		 */
-		clause = replace_nestloop_params(root, clause);
+		/* percollists must correspond to index columns */
+		Assert(list_length(percollists) <= index->ncolumns);
 
-		if (IsA(clause, OpExpr))
+		indexcol = 0;
+		foreach(lc2, percollists)
 		{
-			OpExpr	   *op = (OpExpr *) clause;
+			List	   *percollist = (List *) lfirst(lc2);
 
-			if (list_length(op->args) != 2)
-				elog(ERROR, "indexorderby clause is not binary opclause");
+			if (percollist != NIL)
+			{
+				Node	   *clause = (Node *) linitial(percollist);
 
-			/*
-			 * Now, determine which index attribute this is and change the
-			 * indexkey operand as needed.
-			 */
-			linitial(op->args) = fix_indexqual_operand(linitial(op->args),
-													   index);
-		}
-		else
-			elog(ERROR, "unsupported indexorderby type: %d",
-				 (int) nodeTag(clause));
+				/* Should have only one clause per index column */
+				Assert(list_length(percollist) == 1);
+
+				/*
+				 * Replace any outer-relation variables with nestloop params.
+				 *
+				 * This also makes a copy of the clause, so it's safe to
+				 * modify it in-place below.
+				 */
+				clause = replace_nestloop_params(root, clause);
+
+				if (IsA(clause, OpExpr))
+				{
+					OpExpr	   *op = (OpExpr *) clause;
+
+					if (list_length(op->args) != 2)
+						elog(ERROR, "indexorderby clause is not binary opclause");
+
+					/*
+					 * Now replace the indexkey expression with an index Var.
+					 */
+					linitial(op->args) = fix_indexqual_operand(linitial(op->args),
+															   index,
+															   indexcol);
+				}
+				else
+					elog(ERROR, "unsupported indexorderby type: %d",
+						 (int) nodeTag(clause));
+
+				fixed_indexorderbys = lappend(fixed_indexorderbys, clause);
+			}
 
-		fixed_indexorderbys = lappend(fixed_indexorderbys, clause);
+			indexcol++;
+		}
 	}
 
 	return fixed_indexorderbys;
  *
  * We represent index keys by Var nodes having varno == INDEX_VAR and varattno
  * equal to the index's attribute number (index column position).
+ *
+ * Most of the code here is just for sanity cross-checking that the given
+ * expression actually matches the index column it's claimed to.
  */
 static Node *
-fix_indexqual_operand(Node *node, IndexOptInfo *index)
+fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol)
 {
 	Var		   *result;
 	int			pos;
 	if (IsA(node, RelabelType))
 		node = (Node *) ((RelabelType *) node)->arg;
 
-	if (IsA(node, Var) &&
-		((Var *) node)->varno == index->rel->relid)
-	{
-		/* Try to match against simple index columns */
-		int			varatt = ((Var *) node)->varattno;
+	Assert(indexcol >= 0 && indexcol < index->ncolumns);
 
-		if (varatt != 0)
+	if (index->indexkeys[indexcol] != 0)
+	{
+		/* It's a simple index column */
+		if (IsA(node, Var) &&
+			((Var *) node)->varno == index->rel->relid &&
+			((Var *) node)->varattno == index->indexkeys[indexcol])
 		{
-			for (pos = 0; pos < index->ncolumns; pos++)
-			{
-				if (index->indexkeys[pos] == varatt)
-				{
-					result = (Var *) copyObject(node);
-					result->varno = INDEX_VAR;
-					result->varattno = pos + 1;
-					return (Node *) result;
-				}
-			}
+			result = (Var *) copyObject(node);
+			result->varno = INDEX_VAR;
+			result->varattno = indexcol + 1;
+			return (Node *) result;
 		}
+		else
+			elog(ERROR, "index key does not match expected index column");
 	}
 
-	/* Try to match against index expressions */
+	/* It's an index expression, so find and cross-check the expression */
 	indexpr_item = list_head(index->indexprs);
 	for (pos = 0; pos < index->ncolumns; pos++)
 	{
 		if (index->indexkeys[pos] == 0)
 		{
-			Node	   *indexkey;
-
 			if (indexpr_item == NULL)
 				elog(ERROR, "too few entries in indexprs list");
-			indexkey = (Node *) lfirst(indexpr_item);
-			if (indexkey && IsA(indexkey, RelabelType))
-				indexkey = (Node *) ((RelabelType *) indexkey)->arg;
-			if (equal(node, indexkey))
+			if (pos == indexcol)
 			{
-				/* Found a match */
-				result = makeVar(INDEX_VAR, pos + 1,
-								 exprType(lfirst(indexpr_item)), -1,
-								 exprCollation(lfirst(indexpr_item)),
-								 0);
-				return (Node *) result;
+				Node	   *indexkey;
+
+				indexkey = (Node *) lfirst(indexpr_item);
+				if (indexkey && IsA(indexkey, RelabelType))
+					indexkey = (Node *) ((RelabelType *) indexkey)->arg;
+				if (equal(node, indexkey))
+				{
+					result = makeVar(INDEX_VAR, indexcol + 1,
+									 exprType(lfirst(indexpr_item)), -1,
+									 exprCollation(lfirst(indexpr_item)),
+									 0);
+					return (Node *) result;
+				}
+				else
+					elog(ERROR, "index key does not match expected index column");
 			}
 			indexpr_item = lnext(indexpr_item);
 		}
 	}
 
 	/* Ooops... */
-	elog(ERROR, "node is not an index attribute");
+	elog(ERROR, "index key does not match expected index column");
 	return NULL;				/* keep compiler quiet */
 }
 

File src/backend/optimizer/util/pathnode.c

  * 'index' is a usable index.
  * 'clause_groups' is a list of lists of RestrictInfo nodes
  *			to be used as index qual conditions in the scan.
- * 'indexorderbys' is a list of bare expressions (no RestrictInfos)
- *			to be used as index ordering operators in the scan.
+ * 'indexorderbys' is a list of lists of lists of bare expressions (not
+ *			RestrictInfos) to be used as index ordering operators.
  * 'pathkeys' describes the ordering of the path.
  * 'indexscandir' is ForwardScanDirection or BackwardScanDirection
  *			for an ordered index, or NoMovementScanDirection for

File src/backend/optimizer/util/restrictinfo.c

  * being used in an inner indexscan need not be checked again at the join.
  *
  * "Redundant" means either equal() or derived from the same EquivalenceClass.
- * We have to check the latter because indxqual.c may select different derived
+ * We have to check the latter because indxpath.c may select different derived
  * clauses than were selected by generate_join_implied_equalities().
  *
  * Note that we are *not* checking for local redundancies within the given

File src/backend/utils/adt/selfuncs.c

 	List	   *selectivityQuals;
 	ListCell   *l;
 
+	/*
+	 * For our purposes here, it doesn't matter which index columns the
+	 * individual quals and order-by expressions go with, so flatten the
+	 * lists for convenience.
+	 */
+	indexQuals = flatten_clausegroups_list(indexQuals);
+	indexOrderBys = flatten_indexorderbys_list(indexOrderBys);
+
 	/*----------
 	 * If the index is partial, AND the index predicate with the explicitly
 	 * given indexquals to produce a more accurate idea of the index
 			if (!predicate_implied_by(oneQual, indexQuals))
 				predExtraQuals = list_concat(predExtraQuals, oneQual);
 		}
-		/* list_concat avoids modifying the passed-in indexQuals list */
+		/* list_concat avoids modifying the indexQuals list */
 		selectivityQuals = list_concat(predExtraQuals, indexQuals);
 	}
 	else
 	bool		found_saop;
 	bool		found_is_null_op;
 	double		num_sa_scans;
-	ListCell   *l;
+	ListCell   *lc1;
 
 	/*
 	 * For a btree scan, only leading '=' quals plus inequality quals for the
 	 * the index scan).  Additional quals can suppress visits to the heap, so
 	 * it's OK to count them in indexSelectivity, but they should not count
 	 * for estimating numIndexTuples.  So we must examine the given indexQuals
-	 * to find out which ones count as boundary quals.	We rely on the
-	 * knowledge that they are given in index column order.
+	 * to find out which ones count as boundary quals.
 	 *
 	 * For a RowCompareExpr, we consider only the first column, just as
 	 * rowcomparesel() does.
 	 * considered to act the same as it normally does.
 	 */
 	indexBoundQuals = NIL;
-	indexcol = 0;
 	eqQualHere = false;
 	found_saop = false;
 	found_is_null_op = false;
 	num_sa_scans = 1;
-	foreach(l, indexQuals)
+
+	/* clausegroups must correspond to index columns */
+	Assert(list_length(indexQuals) <= index->ncolumns);
+
+	indexcol = 0;
+	foreach(lc1, indexQuals)
 	{
-		RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
-		Expr	   *clause;
-		Node	   *leftop,
-				   *rightop;
-		Oid			clause_op;
-		int			op_strategy;
-		bool		is_null_op = false;
+		List	   *clausegroup = (List *) lfirst(lc1);
+		ListCell   *lc2;
 
-		Assert(IsA(rinfo, RestrictInfo));
-		clause = rinfo->clause;
-		if (IsA(clause, OpExpr))
-		{
-			leftop = get_leftop(clause);
-			rightop = get_rightop(clause);
-			clause_op = ((OpExpr *) clause)->opno;
-		}
-		else if (IsA(clause, RowCompareExpr))
-		{
-			RowCompareExpr *rc = (RowCompareExpr *) clause;
+		eqQualHere = false;
 
-			leftop = (Node *) linitial(rc->largs);
-			rightop = (Node *) linitial(rc->rargs);
-			clause_op = linitial_oid(rc->opnos);
-		}
-		else if (IsA(clause, ScalarArrayOpExpr))
+		foreach(lc2, clausegroup)
 		{
-			ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+			RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc2);
+			Expr	   *clause;
+			Node	   *leftop,
+					   *rightop;
+			Oid			clause_op;
+			int			op_strategy;
+			bool		is_null_op = false;
+
+			Assert(IsA(rinfo, RestrictInfo));
+			clause = rinfo->clause;
+			if (IsA(clause, OpExpr))
+			{
+				leftop = get_leftop(clause);
+				rightop = get_rightop(clause);
+				clause_op = ((OpExpr *) clause)->opno;
+			}
+			else if (IsA(clause, RowCompareExpr))
+			{
+				RowCompareExpr *rc = (RowCompareExpr *) clause;
 
-			leftop = (Node *) linitial(saop->args);
-			rightop = (Node *) lsecond(saop->args);
-			clause_op = saop->opno;
-			found_saop = true;
-		}
-		else if (IsA(clause, NullTest))
-		{
-			NullTest   *nt = (NullTest *) clause;
+				leftop = (Node *) linitial(rc->largs);
+				rightop = (Node *) linitial(rc->rargs);
+				clause_op = linitial_oid(rc->opnos);
+			}
+			else if (IsA(clause, ScalarArrayOpExpr))
+			{
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
 
-			leftop = (Node *) nt->arg;
-			rightop = NULL;
-			clause_op = InvalidOid;
-			if (nt->nulltesttype == IS_NULL)
+				leftop = (Node *) linitial(saop->args);
+				rightop = (Node *) lsecond(saop->args);
+				clause_op = saop->opno;
+				found_saop = true;
+			}
+			else if (IsA(clause, NullTest))
 			{
-				found_is_null_op = true;
-				is_null_op = true;
+				NullTest   *nt = (NullTest *) clause;
+
+				leftop = (Node *) nt->arg;
+				rightop = NULL;
+				clause_op = InvalidOid;
+				if (nt->nulltesttype == IS_NULL)
+				{
+					found_is_null_op = true;
+					is_null_op = true;
+				}
 			}
-		}
-		else
-		{
-			elog(ERROR, "unsupported indexqual type: %d",
-				 (int) nodeTag(clause));
-			continue;			/* keep compiler quiet */
-		}
-		if (match_index_to_operand(leftop, indexcol, index))
-		{
-			/* clause_op is correct */
-		}
-		else if (match_index_to_operand(rightop, indexcol, index))
-		{
-			/* Must flip operator to get the opfamily member */
-			clause_op = get_commutator(clause_op);
-		}
-		else
-		{
-			/* Must be past the end of quals for indexcol, try next */
-			if (!eqQualHere)
-				break;			/* done if no '=' qual for indexcol */
-			indexcol++;
-			eqQualHere = false;
+			else
+			{
+				elog(ERROR, "unsupported indexqual type: %d",
+					 (int) nodeTag(clause));
+				continue;			/* keep compiler quiet */
+			}
+
 			if (match_index_to_operand(leftop, indexcol, index))
 			{
 				/* clause_op is correct */
 			}
-			else if (match_index_to_operand(rightop, indexcol, index))
+			else
 			{
+				Assert(match_index_to_operand(rightop, indexcol, index));
 				/* Must flip operator to get the opfamily member */
 				clause_op = get_commutator(clause_op);
 			}
-			else
+
+			/* check for equality operator */
+			if (OidIsValid(clause_op))
 			{
-				/* No quals for new indexcol, so we are done */
-				break;
-			}
-		}
-		/* check for equality operator */
-		if (OidIsValid(clause_op))
-		{
-			op_strategy = get_op_opfamily_strategy(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)
+				Assert(op_strategy != 0);	/* not a member of opfamily?? */
+				if (op_strategy == BTEqualStrategyNumber)
+					eqQualHere = true;
+			}
+			else if (is_null_op)
+			{
+				/* IS NULL is like = for selectivity determination */
 				eqQualHere = true;
-		}
-		else if (is_null_op)
-		{
-			/* IS NULL is like = for purposes of selectivity determination */
-			eqQualHere = true;
-		}
-		/* count up number of SA scans induced by indexBoundQuals only */
-		if (IsA(clause, ScalarArrayOpExpr))
-		{
-			ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
-			int			alength = estimate_array_length(lsecond(saop->args));
+			}
+			/* count up number of SA scans induced by indexBoundQuals only */
+			if (IsA(clause, ScalarArrayOpExpr))
+			{
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) clause;
+				int		alength = estimate_array_length(lsecond(saop->args));
 
-			if (alength > 1)
-				num_sa_scans *= alength;
+				if (alength > 1)
+					num_sa_scans *= alength;
+			}
+			indexBoundQuals = lappend(indexBoundQuals, rinfo);
 		}
-		indexBoundQuals = lappend(indexBoundQuals, rinfo);
+
+		/* Done with this indexcol, continue to next only if it had = qual */
+		if (!eqQualHere)
+			break;
+
+		indexcol++;
 	}
 
 	/*
 	 * NullTest invalidates that theory, even though it sets eqQualHere.
 	 */
 	if (index->unique &&
-		indexcol == index->ncolumns - 1 &&
+		indexcol == index->ncolumns &&
 		eqQualHere &&
 		!found_saop &&
 		!found_is_null_op)
 	GinStatsData ginStats;
 
 	/*
+	 * For our purposes here, it doesn't matter which index columns the
+	 * individual quals and order-by expressions go with, so flatten the
+	 * lists for convenience.
+	 */
+	indexQuals = flatten_clausegroups_list(indexQuals);
+	indexOrderBys = flatten_indexorderbys_list(indexOrderBys);
+
+	/*
 	 * Obtain statistic information from the meta page
 	 */
 	indexRel = index_open(index->indexoid, AccessShareLock);
 			if (!predicate_implied_by(oneQual, indexQuals))
 				predExtraQuals = list_concat(predExtraQuals, oneQual);
 		}
-		/* list_concat avoids modifying the passed-in indexQuals list */
+		/* list_concat avoids modifying the indexQuals list */
 		selectivityQuals = list_concat(predExtraQuals, indexQuals);
 	}
 	else

File src/include/nodes/relation.h

  * AND semantics across the list.  Each clause is a RestrictInfo node from
  * the query's WHERE or JOIN conditions.
  *
- * 'indexquals' has the same structure as 'indexclauses', but it contains
- * the actual indexqual conditions that can be used with the index.
- * In simple cases this is identical to 'indexclauses', but when special
- * indexable operators appear in 'indexclauses', they are replaced by the
- * derived indexscannable conditions in 'indexquals'.
- *
- * 'indexorderbys', if not NIL, is a list of ORDER BY expressions that have
- * been found to be usable as ordering operators for an amcanorderbyop index.
- * Note that these are not RestrictInfos, just bare expressions, since they
- * generally won't yield booleans.  The list will match the path's pathkeys.
- * Also, unlike the case for quals, it's guaranteed that each expression has
- * the index key on the left side of the operator.
+ * 'indexquals' is a list of sub-lists of the actual index qual conditions
+ * that can be used with the index.  There is one possibly-empty sub-list
+ * for each index column (but empty sub-lists for trailing columns can be
+ * omitted).  The qual conditions are RestrictInfos, and in simple cases
+ * are the same RestrictInfos that appear in the flat indexclauses list.
+ * But when special indexable operators appear in 'indexclauses', they are
+ * replaced by their derived indexscannable conditions in 'indexquals'.
+ * Note that an entirely empty indexquals list denotes a full-index scan.
+ *
+ * 'indexorderbys', if not NIL, is a list of lists of lists of ORDER BY
+ * expressions that have been found to be usable as ordering operators for an
+ * amcanorderbyop index.  These are not RestrictInfos, just bare expressions,
+ * since they generally won't yield booleans.  Also, unlike the case for
+ * quals, it's guaranteed that each expression has the index key on the left
+ * side of the operator.  The top list has one entry per pathkey in the
+ * path's pathkeys, and the sub-lists have one sub-sublist per index column.
+ * This representation is a bit of overkill, since there will be only one
+ * actual expression per pathkey, but it's convenient because each sub-list
+ * has the same structure as the indexquals list.
  *
  * 'isjoininner' is TRUE if the path is a nestloop inner scan (that is,
  * some of the index conditions are join rather than restriction clauses).

File src/include/optimizer/paths.h

 							List *clausegroups);
 extern void check_partial_indexes(PlannerInfo *root, RelOptInfo *rel);
 extern List *flatten_clausegroups_list(List *clausegroups);
+extern List *flatten_indexorderbys_list(List *indexorderbys);
+extern Expr *adjust_rowcompare_for_index(RowCompareExpr *clause,
+							IndexOptInfo *index,
+							int indexcol,
+							List **indexcolnos,
+							bool *var_on_left_p);
 
 /*
  * orindxpath.c

File src/test/regress/expected/create_index.out

 RESET enable_indexscan;
 RESET enable_bitmapscan;
 DROP TABLE onek_with_null;
+--
+-- Check behavior with duplicate index column contents
+--
+CREATE TABLE dupindexcols AS
+  SELECT unique1 as id, stringu2::text as f1 FROM tenk1;
+CREATE INDEX dupindexcols_i ON dupindexcols (f1, id, f1 text_pattern_ops);
+VACUUM ANALYZE dupindexcols;
+EXPLAIN (COSTS OFF)
+  SELECT count(*) FROM dupindexcols
+    WHERE f1 > 'LX' and id < 1000 and f1 ~<~ 'YX';
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Aggregate
+   ->  Index Only Scan using dupindexcols_i on dupindexcols
+         Index Cond: ((f1 > 'LX'::text) AND (id < 1000) AND (f1 ~<~ 'YX'::text))
+(3 rows)
+
+SELECT count(*) FROM dupindexcols
+  WHERE f1 > 'LX' and id < 1000 and f1 ~<~ 'YX';
+ count 
+-------
+   500
+(1 row)
+

File src/test/regress/expected/sanity_check.out

  default_tbl             | f
  defaultexpr_tbl         | f
  dept                    | f
+ dupindexcols            | t
  e_star                  | f
  emp                     | f
  equipment_r             | f
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(153 rows)
+(154 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have

File src/test/regress/output/misc.source

  default_tbl
  defaultexpr_tbl
  dept
+ dupindexcols
  e_star
  emp
  equipment_r
  toyemp
  varchar_tbl
  xacttest
-(107 rows)
+(108 rows)
 
 SELECT name(equipment(hobby_construct(text 'skywalking', text 'mer')));
  name 

File src/test/regress/sql/create_index.sql

 RESET enable_bitmapscan;
 
 DROP TABLE onek_with_null;
+
+--
+-- Check behavior with duplicate index column contents
+--
+
+CREATE TABLE dupindexcols AS
+  SELECT unique1 as id, stringu2::text as f1 FROM tenk1;
+CREATE INDEX dupindexcols_i ON dupindexcols (f1, id, f1 text_pattern_ops);
+VACUUM ANALYZE dupindexcols;
+
+EXPLAIN (COSTS OFF)
+  SELECT count(*) FROM dupindexcols
+    WHERE f1 > 'LX' and id < 1000 and f1 ~<~ 'YX';
+SELECT count(*) FROM dupindexcols
+  WHERE f1 > 'LX' and id < 1000 and f1 ~<~ 'YX';