pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
From: Alvaro Herrera <alvherre@2ndquadrant.com>
To: Pg Hackers <pgsql-hackers@postgresql.org>
Subject: Re: Indirect indexes
Date: Tue, 1 Nov 2016 01:43:31 -0300
Message-ID: <20161101044331.4w73ywvi42niltva@alvherre.pgsql> (raw)
In-Reply-To: <20161018182843.xczrxsa2yd47pnru@alvherre.pgsql>
List-Unsubscribe: <mailto:majordomo@postgresql.org?body=unsub%20pgsql-hackers>

Alvaro Herrera wrote:
> I propose we introduce the concept of "indirect indexes".

This is a WIP non-functional patch for indirect indexes.  I've been
distracted from working on it for some time already and will be off-line
for half this month yet, but since this was discussed and seems to be
considered a welcome idea, I am posting it for those who want to have a
look at what I'm doing.  This can write values to indirect indexes (only
btrees), but it cannot read values from them yet, so don't expect this
to work at all unless you are hoping to read index files using
pageinspect.  (If you do test this, be aware that "VACUUM FULL pg_class"
is a case that I know needs fixed.)

I think the most interesting change here is how
HeapSatisfiesHOTandKeyUpdate() has accomodated some additional code to
return a bitmapset of columns that are not modified by an update.

This implements a new command
  CREATE INDIRECT INDEX
which instructs the AM to create an index that stores primary key values
instead of CTID values.  I have not tried yet to remove the limitation
of only six bytes in the PK value.  The part of the patch I'm not
submitting just yet adds a flag to IndexInfo used by IndexPath, so that
when the index is selected by the planner, an IndirectIndexScan node is
created instead of a plain IndexScan.  This node knows how to invoke the
AM so that the PK values are extracted in a first step and the CTIDs are
extracted from the PK in a second step (IndirectIndexScan carries two
IndexScanDesc structs and two index RelationDescs, so it keeps both the
indirect index and the PK index open).

The part that generated the most discussion was vacuuming.  As I said
earlier, I think that instead of trying to shoehorn an index cleanup in
regular vacuum (and cause a terrible degradation of maintenance_work_mem
consumption, into optimizing which so much work has gone), these indexes
would rely on desultory cleanup instead through the "killtuple"
interface that causes index tuples to be removed during scan.  Timely
cleanup is not critical as it is with regular (direct) indexes, given
that CTIDs are not stored and thus tuple movement does not affect this
type of indexes.

This patch is considerably smaller than the toy patch I had, which
introduced a separate AM for "ibtree" -- that was duplicating a lot
of code and adding more code complexity, which becomes much simpler with
the approach in the current code; one thing I didn't like at all was the
fact that the ibtree routines were calling the "internal" btree
routines, which was not nice.  (Also, it would have meant having
duplicate operator family/class rows.)

I hope to be back at home to collaborate with the commitfest on Nov
14th.

-- 
Álvaro Herrera                https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1b45a4c..9f899c7 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -92,6 +92,7 @@ brinhandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = true;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = brinbuild;
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index f07eedc..1bc91d2 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -49,6 +49,7 @@ ginhandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = true;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = ginbuild;
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index b8aa9bc..4ec34d5 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -69,6 +69,7 @@ gisthandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = true;
 	amroutine->amclusterable = true;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = gistbuild;
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index e3b1eef..3fa3d71 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -66,6 +66,7 @@ hashhandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = false;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = INT4OID;
 
 	amroutine->ambuild = hashbuild;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index b019bc1..9e91b41 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -96,10 +96,10 @@ static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf,
 				HeapTuple newtup, HeapTuple old_key_tup,
 				bool all_visible_cleared, bool new_all_visible_cleared);
 static void HeapSatisfiesHOTandKeyUpdate(Relation relation,
-							 Bitmapset *hot_attrs,
-							 Bitmapset *key_attrs, Bitmapset *id_attrs,
+							 Bitmapset *hot_attrs, Bitmapset *key_attrs,
+							 Bitmapset *id_attrs, Bitmapset *indirect_attrs,
 							 bool *satisfies_hot, bool *satisfies_key,
-							 bool *satisfies_id,
+							 bool *satisfies_id, Bitmapset **unchanged_attrs,
 							 HeapTuple oldtup, HeapTuple newtup);
 static bool heap_acquire_tuplock(Relation relation, ItemPointer tid,
 					 LockTupleMode mode, LockWaitPolicy wait_policy,
@@ -3414,6 +3414,8 @@ simple_heap_delete(Relation relation, ItemPointer tid)
  *	crosscheck - if not InvalidSnapshot, also check old tuple against this
  *	wait - true if should wait for any conflicting update to commit/abort
  *	hufd - output parameter, filled in failure cases (see below)
+ *	unchanged_ind_cols - output parameter; bits set for unmodified columns
+ *		that are indexed by indirect indexes
  *	lockmode - output parameter, filled with lock mode acquired on tuple
  *
  * Normal, successful return value is HeapTupleMayBeUpdated, which
@@ -3436,13 +3438,15 @@ simple_heap_delete(Relation relation, ItemPointer tid)
 HTSU_Result
 heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 			CommandId cid, Snapshot crosscheck, bool wait,
-			HeapUpdateFailureData *hufd, LockTupleMode *lockmode)
+			HeapUpdateFailureData *hufd, Bitmapset **unchanged_ind_cols,
+			LockTupleMode *lockmode)
 {
 	HTSU_Result result;
 	TransactionId xid = GetCurrentTransactionId();
 	Bitmapset  *hot_attrs;
 	Bitmapset  *key_attrs;
 	Bitmapset  *id_attrs;
+	Bitmapset  *indirect_attrs;
 	ItemId		lp;
 	HeapTupleData oldtup;
 	HeapTuple	heaptup;
@@ -3504,6 +3508,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY);
 	id_attrs = RelationGetIndexAttrBitmap(relation,
 										  INDEX_ATTR_BITMAP_IDENTITY_KEY);
+	indirect_attrs = RelationGetIndexAttrBitmap(relation,
+												INDEX_ATTR_BITMAP_INDIRECT_INDEXES);
 
 	block = ItemPointerGetBlockNumber(otid);
 	buffer = ReadBuffer(relation, block);
@@ -3561,9 +3567,11 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	 * is updates that don't manipulate key columns, not those that
 	 * serendipitiously arrive at the same key values.
 	 */
-	HeapSatisfiesHOTandKeyUpdate(relation, hot_attrs, key_attrs, id_attrs,
+	HeapSatisfiesHOTandKeyUpdate(relation,
+								 hot_attrs, key_attrs, id_attrs, indirect_attrs,
 								 &satisfies_hot, &satisfies_key,
-								 &satisfies_id, &oldtup, newtup);
+								 &satisfies_id, unchanged_ind_cols,
+								 &oldtup, newtup);
 	if (satisfies_key)
 	{
 		*lockmode = LockTupleNoKeyExclusive;
@@ -3803,6 +3811,7 @@ l2:
 		bms_free(hot_attrs);
 		bms_free(key_attrs);
 		bms_free(id_attrs);
+		bms_free(indirect_attrs);
 		return result;
 	}
 
@@ -4356,41 +4365,53 @@ heap_tuple_attr_equals(TupleDesc tupdesc, int attrnum,
  * Check which columns are being updated.
  *
  * This simultaneously checks conditions for HOT updates, for FOR KEY
- * SHARE updates, and REPLICA IDENTITY concerns.  Since much of the time they
- * will be checking very similar sets of columns, and doing the same tests on
- * them, it makes sense to optimize and do them together.
+ * SHARE updates, for REPLICA IDENTITY concerns, and for indirect indexing
+ * concerns.  Since much of the time they will be checking very similar sets of
+ * columns, and doing the same tests on them, it makes sense to optimize and do
+ * them together.
  *
- * We receive three bitmapsets comprising the three sets of columns we're
+ * We receive four bitmapsets comprising the four sets of columns we're
  * interested in.  Note these are destructively modified; that is OK since
  * this is invoked at most once in heap_update.
  *
- * hot_result is set to TRUE if it's okay to do a HOT update (i.e. it does not
- * modified indexed columns); key_result is set to TRUE if the update does not
- * modify columns used in the key; id_result is set to TRUE if the update does
- * not modify columns in any index marked as the REPLICA IDENTITY.
+ * satisfies_hot is set to TRUE if it's okay to do a HOT update (i.e. it does
+ * not modified indexed columns); satisfies_key is set to TRUE if the update
+ * does not modify columns used in the key; satisfies_id is set to TRUE if the
+ * update does not modify columns in any index marked as the REPLICA IDENTITY.
+ *
+ * unchanged_attrs is an output bitmapset that has a bit set if the
+ * corresponding column is indexed by an indirect index and is not modified.
+ * Note that because system columns cannot be indexed by indirect indexes,
+ * these values are not shifted by FirstLowInvalidHeapAttributeNumber.
  */
 static void
 HeapSatisfiesHOTandKeyUpdate(Relation relation, Bitmapset *hot_attrs,
 							 Bitmapset *key_attrs, Bitmapset *id_attrs,
+							 Bitmapset *indirect_attrs,
 							 bool *satisfies_hot, bool *satisfies_key,
-							 bool *satisfies_id,
+							 bool *satisfies_id, Bitmapset **unchanged_attrs,
 							 HeapTuple oldtup, HeapTuple newtup)
 {
 	int			next_hot_attnum;
 	int			next_key_attnum;
 	int			next_id_attnum;
+	int			next_indirect_attnum;
 	bool		hot_result = true;
 	bool		key_result = true;
 	bool		id_result = true;
+	bool		collecting_indirect;
 
 	/* If REPLICA IDENTITY is set to FULL, id_attrs will be empty. */
 	Assert(bms_is_subset(id_attrs, key_attrs));
-	Assert(bms_is_subset(key_attrs, hot_attrs));
+
+	/* Waste no time on indirect indexes if there aren't any */
+	collecting_indirect = (unchanged_attrs != NULL) &&
+		!bms_is_empty(indirect_attrs);
 
 	/*
 	 * If one of these sets contains no remaining bits, bms_first_member will
 	 * return -1, and after adding FirstLowInvalidHeapAttributeNumber (which
-	 * is negative!)  we'll get an attribute number that can't possibly be
+	 * is negative!) we'll get an attribute number that can't possibly be
 	 * real, and thus won't match any actual attribute number.
 	 */
 	next_hot_attnum = bms_first_member(hot_attrs);
@@ -4399,24 +4420,43 @@ HeapSatisfiesHOTandKeyUpdate(Relation relation, Bitmapset *hot_attrs,
 	next_key_attnum += FirstLowInvalidHeapAttributeNumber;
 	next_id_attnum = bms_first_member(id_attrs);
 	next_id_attnum += FirstLowInvalidHeapAttributeNumber;
+	next_indirect_attnum = bms_first_member(indirect_attrs);
+	next_indirect_attnum += FirstLowInvalidHeapAttributeNumber;
 
 	for (;;)
 	{
 		bool		changed;
 		int			check_now;
 
+		/* Set to a value greater than what can be attained normally */
+		check_now = MaxAttrNumber + 1;
+
 		/*
-		 * Since the HOT attributes are a superset of the key attributes and
-		 * the key attributes are a superset of the id attributes, this logic
-		 * is guaranteed to identify the next column that needs to be checked.
+		 * Since the key attributes are a superset of the id attributes, this
+		 * logic is guaranteed to identify the next column that needs to be
+		 * checked.  We consider indirect attributes and HOT attributes
+		 * separately because they aren't supersets of anything.
 		 */
-		if (hot_result && next_hot_attnum > FirstLowInvalidHeapAttributeNumber)
-			check_now = next_hot_attnum;
-		else if (key_result && next_key_attnum > FirstLowInvalidHeapAttributeNumber)
+		if (key_result && next_key_attnum > FirstLowInvalidHeapAttributeNumber)
 			check_now = next_key_attnum;
 		else if (id_result && next_id_attnum > FirstLowInvalidHeapAttributeNumber)
 			check_now = next_id_attnum;
-		else
+
+		if (hot_result && next_hot_attnum > FirstLowInvalidHeapAttributeNumber)
+		{
+			if (next_hot_attnum < check_now)
+				check_now = next_hot_attnum;
+		}
+
+		if (collecting_indirect)
+		{
+			Assert(next_indirect_attnum > 0);
+			if (next_indirect_attnum < check_now)
+				check_now = next_indirect_attnum;
+		}
+
+		/* are we done? */
+		if (check_now == MaxAttrNumber + 1)
 			break;
 
 		/* See whether it changed. */
@@ -4430,13 +4470,33 @@ HeapSatisfiesHOTandKeyUpdate(Relation relation, Bitmapset *hot_attrs,
 				key_result = false;
 			if (check_now == next_id_attnum)
 				id_result = false;
-
-			/* if all are false now, we can stop checking */
-			if (!hot_result && !key_result && !id_result)
-				break;
 		}
 
 		/*
+		 * Deal with indirect indexes.  Set the bit in the output set if the
+		 * column did not change, and compute the next column, which is
+		 * necessary to determine whether we need to iterate again.
+		 */
+		if (collecting_indirect && check_now == next_indirect_attnum)
+		{
+			if (!changed)
+				*unchanged_attrs = bms_add_member(*unchanged_attrs,
+												  check_now);
+			next_indirect_attnum = bms_first_member(indirect_attrs);
+			/* are we done with these? */
+			if (next_indirect_attnum == -1)
+				collecting_indirect = false;
+			next_indirect_attnum += FirstLowInvalidHeapAttributeNumber;
+		}
+
+		/*
+		 * if HOT, key, id are all false now and we're not collecting
+		 * unchanged indirect index columns, we can stop checking.
+		 */
+		if (!hot_result && !key_result && !id_result && !collecting_indirect)
+			break;
+
+		/*
 		 * Advance the next attribute numbers for the sets that contain the
 		 * attribute we just checked.  As we work our way through the columns,
 		 * the next_attnum values will rise; but when each set becomes empty,
@@ -4483,7 +4543,7 @@ simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup)
 	result = heap_update(relation, otid, tup,
 						 GetCurrentCommandId(true), InvalidSnapshot,
 						 true /* wait for commit */ ,
-						 &hufd, &lockmode);
+						 &hufd, NULL, &lockmode);
 	switch (result)
 	{
 		case HeapTupleSelfUpdated:
diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c
index fc4702c..07bf463 100644
--- a/src/backend/access/heap/tuptoaster.c
+++ b/src/backend/access/heap/tuptoaster.c
@@ -1614,6 +1614,7 @@ toast_save_datum(Relation rel, Datum value,
 			/* Only index relations marked as ready can be updated */
 			if (IndexIsReady(toastidxs[i]->rd_index))
 				index_insert(toastidxs[i], t_values, t_isnull,
+							 NULL,
 							 &(toasttup->t_self),
 							 toastrel,
 							 toastidxs[i]->rd_index->indisunique ?
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 54b71cb..8b114ab 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -189,10 +189,13 @@ bool
 index_insert(Relation indexRelation,
 			 Datum *values,
 			 bool *isnull,
+			 Datum *pkeyValues,
 			 ItemPointer heap_t_ctid,
 			 Relation heapRelation,
 			 IndexUniqueCheck checkUnique)
 {
+	ItemPointerData iptr;
+
 	RELATION_CHECKS;
 	CHECK_REL_PROCEDURE(aminsert);
 
@@ -201,8 +204,19 @@ index_insert(Relation indexRelation,
 									   (HeapTuple) NULL,
 									   InvalidBuffer);
 
+	/*
+	 * Indirect indexes use a fake item pointer constructed from the primary
+	 * key values; regular indexes store the actual heap item pointer.
+	 */
+	if (!indexRelation->rd_index->indisindirect)
+		ItemPointerCopy(heap_t_ctid, &iptr);
+	else
+		FAKE_CTID_FROM_PKVALUES(&iptr,
+								indexRelation->rd_index->indnatts,
+								pkeyValues);
+
 	return indexRelation->rd_amroutine->aminsert(indexRelation, values, isnull,
-												 heap_t_ctid, heapRelation,
+												 &iptr, heapRelation,
 												 checkUnique);
 }
 
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index ef69290..eb4beef 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -92,7 +92,9 @@ static void _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel);
  *		By here, itup is filled in, including the TID.
  *
  *		If checkUnique is UNIQUE_CHECK_NO or UNIQUE_CHECK_PARTIAL, this
- *		will allow duplicates.  Otherwise (UNIQUE_CHECK_YES or
+ *		will allow duplicates.  If it's UNIQUE_CHECK_INSERT_SINGLETON, the value
+ *		will only be inserted if there isn't already a tuple with that value.
+ *		Otherwise (UNIQUE_CHECK_YES or
  *		UNIQUE_CHECK_EXISTING) it will throw error for a duplicate.
  *		For UNIQUE_CHECK_EXISTING we merely run the duplicate check, and
  *		don't actually insert.
@@ -100,8 +102,8 @@ static void _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel);
  *		The result value is only significant for UNIQUE_CHECK_PARTIAL:
  *		it must be TRUE if the entry is known unique, else FALSE.
  *		(In the current implementation we'll also return TRUE after a
- *		successful UNIQUE_CHECK_YES or UNIQUE_CHECK_EXISTING call, but
- *		that's just a coding artifact.)
+ *		successful UNIQUE_CHECK_YES, UNIQUE_CHECK_EXISTING or
+ *		UNIQUE_CHECK_INSERT_SINGLETON call, but that's just a coding artifact.)
  */
 bool
 _bt_doinsert(Relation rel, IndexTuple itup,
@@ -138,6 +140,21 @@ top:
 						true, stack, BT_WRITE, NULL);
 
 	/*
+	 * In insert-singleton mode, we must return without doing anything if the
+	 * value we're inserting already exists.
+	 */
+#if 0
+	if (checkUnique == UNIQUE_CHECK_INSERT_SINGLETON)
+	{
+		offset = _bt_binsrch( .. );
+		if (offset is valid and contains a tuple matching the scankey)
+			return true;
+		/* otherwise fall through to insert */
+	}
+#endif
+
+
+	/*
 	 * If we're not allowing duplicates, make sure the key isn't already in
 	 * the index.
 	 *
@@ -158,7 +175,8 @@ top:
 	 * let the tuple in and return false for possibly non-unique, or true for
 	 * definitely unique.
 	 */
-	if (checkUnique != UNIQUE_CHECK_NO)
+	if (checkUnique != UNIQUE_CHECK_NO &&
+		checkUnique != UNIQUE_CHECK_INSERT_SINGLETON)
 	{
 		TransactionId xwait;
 		uint32		speculativeToken;
@@ -167,6 +185,10 @@ top:
 		xwait = _bt_check_unique(rel, itup, heapRel, buf, offset, itup_scankey,
 								 checkUnique, &is_unique, &speculativeToken);
 
+		if (checkUnique == UNIQUE_CHECK_INSERT_SINGLETON &&
+			TransactionIdIsValid(xwait))
+			return true;
+
 		if (TransactionIdIsValid(xwait))
 		{
 			/* Have to wait for the other guy ... */
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 128744c..bbf06d2 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -37,6 +37,7 @@ typedef struct
 {
 	bool		isUnique;
 	bool		haveDead;
+	bool		isIndirect;
 	Relation	heapRel;
 	BTSpool    *spool;
 
@@ -45,6 +46,8 @@ typedef struct
 	 * put into spool2 instead of spool in order to avoid uniqueness check.
 	 */
 	BTSpool    *spool2;
+	int16		pkNumKeys;
+	AttrNumber	pkAttnums[INDEX_MAX_KEYS];
 	double		indtuples;
 } BTBuildState;
 
@@ -98,6 +101,7 @@ bthandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = false;
 	amroutine->amclusterable = true;
 	amroutine->ampredlocks = true;
+	amroutine->amcanindirect = true;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = btbuild;
@@ -136,6 +140,25 @@ btbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	buildstate.heapRel = heap;
 	buildstate.spool = NULL;
 	buildstate.spool2 = NULL;
+	buildstate.isIndirect = indexInfo->ii_IsIndirect;
+	if (indexInfo->ii_IsIndirect)
+	{
+		Oid			pkOid;
+		Relation	pkRel;
+		int			i;
+
+		pkOid = RelationGetPrimaryKey(heap);
+		pkRel = index_open(pkOid, AccessShareLock);
+
+		buildstate.pkNumKeys = pkRel->rd_index->indnatts;
+		for (i = 0; i < buildstate.pkNumKeys; i++)
+			buildstate.pkAttnums[i] = pkRel->rd_index->indkey.values[i];
+		index_close(pkRel, AccessShareLock);
+	}
+	else
+	{
+		buildstate.pkNumKeys = 0;
+	}
 	buildstate.indtuples = 0;
 
 #ifdef BTREE_BUILD_STATS
@@ -213,18 +236,42 @@ btbuildCallback(Relation index,
 				void *state)
 {
 	BTBuildState *buildstate = (BTBuildState *) state;
+	ItemPointerData iptr;
+
+	if (buildstate->isIndirect)
+	{
+		Datum	pkValues[INDEX_MAX_KEYS];
+		int		i;
+		bool	isnull;
+
+		/*
+		 * XXX WAG: this is very slow in the general case, but OK if PK column
+		 * is first.
+		 */
+		for (i = 0; i < buildstate->pkNumKeys; i++)
+		{
+			pkValues[i] = heap_getattr(htup,
+									   buildstate->pkAttnums[i],
+									   RelationGetDescr(buildstate->heapRel),
+									   &isnull);
+			Assert(!isnull);
+		}
+		FAKE_CTID_FROM_PKVALUES(&iptr, buildstate->pkNumKeys, pkValues);
+	}
+	else
+		ItemPointerCopy(&htup->t_self, &iptr);
 
 	/*
 	 * insert the index tuple into the appropriate spool file for subsequent
 	 * processing
 	 */
 	if (tupleIsAlive || buildstate->spool2 == NULL)
-		_bt_spool(buildstate->spool, &htup->t_self, values, isnull);
+		_bt_spool(buildstate->spool, &iptr, values, isnull);
 	else
 	{
 		/* dead tuples are put into spool2 */
 		buildstate->haveDead = true;
-		_bt_spool(buildstate->spool2, &htup->t_self, values, isnull);
+		_bt_spool(buildstate->spool2, &iptr, values, isnull);
 	}
 
 	buildstate->indtuples += 1;
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index d570ae5..9378919 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -48,6 +48,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = false;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = spgbuild;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 08b646d..1f57eb5 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -104,6 +104,7 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 					int16 *coloptions,
 					bool primary,
 					bool isexclusion,
+					bool isindirect,
 					bool immediate,
 					bool isvalid);
 static void index_update_stats(Relation rel,
@@ -554,6 +555,7 @@ UpdateIndexRelation(Oid indexoid,
 					int16 *coloptions,
 					bool primary,
 					bool isexclusion,
+					bool isindirect,
 					bool immediate,
 					bool isvalid)
 {
@@ -623,6 +625,7 @@ UpdateIndexRelation(Oid indexoid,
 	values[Anum_pg_index_indrelid - 1] = ObjectIdGetDatum(heapoid);
 	values[Anum_pg_index_indnatts - 1] = Int16GetDatum(indexInfo->ii_NumIndexAttrs);
 	values[Anum_pg_index_indisunique - 1] = BoolGetDatum(indexInfo->ii_Unique);
+	values[Anum_pg_index_indisindirect - 1] = BoolGetDatum(isindirect);
 	values[Anum_pg_index_indisprimary - 1] = BoolGetDatum(primary);
 	values[Anum_pg_index_indisexclusion - 1] = BoolGetDatum(isexclusion);
 	values[Anum_pg_index_indimmediate - 1] = BoolGetDatum(immediate);
@@ -681,6 +684,7 @@ UpdateIndexRelation(Oid indexoid,
  * coloptions: array of per-index-column indoption settings
  * reloptions: AM-specific options
  * isprimary: index is a PRIMARY KEY
+ * isindirect: index is an indirect one
  * isconstraint: index is owned by PRIMARY KEY, UNIQUE, or EXCLUSION constraint
  * deferrable: constraint is DEFERRABLE
  * initdeferred: constraint is INITIALLY DEFERRED
@@ -710,6 +714,7 @@ index_create(Relation heapRelation,
 			 int16 *coloptions,
 			 Datum reloptions,
 			 bool isprimary,
+			 bool isindirect,
 			 bool isconstraint,
 			 bool deferrable,
 			 bool initdeferred,
@@ -769,6 +774,21 @@ index_create(Relation heapRelation,
 				 errmsg("concurrent index creation on system catalog tables is not supported")));
 
 	/*
+	 * indirect indexes are forbidden on system catalogs, and they obviously cannot
+	 * be primary keys either.
+	 */
+	if (isindirect &&
+		IsSystemRelation(heapRelation))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("indirect index creation on system catalog tables is not supported")));
+	if (isindirect && isprimary)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("primary key indexes cannot be indirect")));
+	/* XXX other restrictions needed? */
+
+	/*
 	 * This case is currently not supported, but there's no way to ask for it
 	 * in the grammar anyway, so it can't happen.
 	 */
@@ -916,7 +936,7 @@ index_create(Relation heapRelation,
 	 */
 	UpdateIndexRelation(indexRelationId, heapRelationId, indexInfo,
 						collationObjectId, classObjectId, coloptions,
-						isprimary, is_exclusion,
+						isprimary, is_exclusion, isindirect,
 						!deferrable,
 						!concurrent);
 
@@ -1011,6 +1031,14 @@ index_create(Relation heapRelation,
 			Assert(!initdeferred);
 		}
 
+		/* Store dependency on primary key index, if needed */
+		if (isindirect)
+		{
+			ObjectAddressSet(referenced, RelationRelationId,
+							 RelationGetPrimaryKey(heapRelation));
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
 		/* Store dependency on collations */
 		/* The default collation is pinned, so don't bother recording it */
 		for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
@@ -1681,6 +1709,7 @@ BuildIndexInfo(Relation index)
 
 	/* other info */
 	ii->ii_Unique = indexStruct->indisunique;
+	ii->ii_IsIndirect = indexStruct->indisindirect;
 	ii->ii_ReadyForInserts = IndexIsReady(indexStruct);
 	/* assume not doing speculative insertion for now */
 	ii->ii_UniqueOps = NULL;
@@ -3161,6 +3190,7 @@ validate_index_heapscan(Relation heapRelation,
 			index_insert(indexRelation,
 						 values,
 						 isnull,
+						 NULL,	/* FIXME need to PK values here */
 						 &rootTuple,
 						 heapRelation,
 						 indexInfo->ii_Unique ?
@@ -3564,7 +3594,10 @@ reindex_relation(Oid relid, int flags, int options)
 
 	/* Ensure rd_indexattr is valid; see comments for RelationSetIndexList */
 	if (is_pg_class)
+	{
+		elog(ERROR, "FIXME must fix this case");
 		(void) RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_ALL);
+	}
 
 	PG_TRY();
 	{
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index b9fe102..f00f446 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -136,6 +136,7 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple)
 		index_insert(relationDescs[i],	/* index relation */
 					 values,	/* array of index Datums */
 					 isnull,	/* is-null flags */
+					 NULL,		/* catalogs never had indirect indexes */
 					 &(heapTuple->t_self),		/* tid of heap tuple */
 					 heapRelation,
 					 relationDescs[i]->rd_index->indisunique ?
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 564e10e..caa25ba 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -331,7 +331,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 				 BTREE_AM_OID,
 				 rel->rd_rel->reltablespace,
 				 collationObjectId, classObjectId, coloptions, (Datum) 0,
-				 true, false, false, false,
+				 true, false, false, false, false,
 				 true, false, false, true, false);
 
 	heap_close(toast_rel, NoLock);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index dc1f79f..13f28d3 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1535,14 +1535,14 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	reindex_relation(OIDOldHeap, reindex_flags, 0);
 
 	/*
-	 * If the relation being rebuild is pg_class, swap_relation_files()
+	 * If the relation being rebuilt is pg_class, swap_relation_files()
 	 * couldn't update pg_class's own pg_class entry (check comments in
 	 * swap_relation_files()), thus relfrozenxid was not updated. That's
 	 * annoying because a potential reason for doing a VACUUM FULL is a
 	 * imminent or actual anti-wraparound shutdown.  So, now that we can
-	 * access the new relation using it's indices, update relfrozenxid.
+	 * access the new relation using its indices, update relfrozenxid.
 	 * pg_class doesn't have a toast relation, so we don't need to update the
-	 * corresponding toast relation. Not that there's little point moving all
+	 * corresponding toast relation. Note that there's little point moving all
 	 * relfrozenxid updates here since swap_relation_files() needs to write to
 	 * pg_class for non-mapped relations anyway.
 	 */
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index 26f9114..5f1a7b7 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -164,7 +164,7 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 		 * correct even if t_self is now dead, because that is the TID the
 		 * index will know about.
 		 */
-		index_insert(indexRel, values, isnull, &(new_row->t_self),
+		index_insert(indexRel, values, isnull, NULL, &(new_row->t_self),
 					 trigdata->tg_relation, UNIQUE_CHECK_EXISTING);
 	}
 	else
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index b4140eb..b9e9e20 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -2535,7 +2535,7 @@ CopyFrom(CopyState cstate)
 				if (resultRelInfo->ri_NumIndices > 0)
 					recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
 														 estate, false, NULL,
-														   NIL);
+														   NIL, NULL);
 
 				/* AFTER ROW INSERT Triggers */
 				ExecARInsertTriggers(estate, resultRelInfo, tuple,
@@ -2649,7 +2649,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 			ExecStoreTuple(bufferedTuples[i], myslot, InvalidBuffer, false);
 			recheckIndexes =
 				ExecInsertIndexTuples(myslot, &(bufferedTuples[i]->t_self),
-									  estate, false, NULL, NIL);
+									  estate, false, NULL, NIL, NULL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
 								 recheckIndexes);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 85817c6..17fbf2d 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -519,6 +519,11 @@ DefineIndex(Oid relationId,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 		errmsg("access method \"%s\" does not support exclusion constraints",
 			   accessMethodName)));
+	if (stmt->isindirect && !amRoutine->amcanindirect)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("access method \"%s\" does not support indirect indexes",
+						accessMethodName)));
 
 	amcanorder = amRoutine->amcanorder;
 	amoptions = amRoutine->amoptions;
@@ -653,7 +658,7 @@ DefineIndex(Oid relationId,
 					 indexInfo, indexColNames,
 					 accessMethodId, tablespaceId,
 					 collationObjectId, classObjectId,
-					 coloptions, reloptions, stmt->primary,
+					 coloptions, reloptions, stmt->primary, stmt->isindirect,
 					 stmt->isconstraint, stmt->deferrable, stmt->initdeferred,
 					 allowSystemTableMods,
 					 skip_build || stmt->concurrent,
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 009c1b7..302fc60 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -273,7 +273,8 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 					  EState *estate,
 					  bool noDupErr,
 					  bool *specConflict,
-					  List *arbiterIndexes)
+					  List *arbiterIndexes,
+					  Bitmapset *unchangedAttrs)
 {
 	List	   *result = NIL;
 	ResultRelInfo *resultRelInfo;
@@ -285,6 +286,7 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 	ExprContext *econtext;
 	Datum		values[INDEX_MAX_KEYS];
 	bool		isnull[INDEX_MAX_KEYS];
+	Datum		pkeyValues[INDEX_MAX_KEYS];
 
 	/*
 	 * Get information from the result relation info structure.
@@ -348,6 +350,27 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 		}
 
 		/*
+		 * For indirect indexes, verify whether the indexed attributes have
+		 * changed; if they have not, skip the insertion.
+		 */
+		if (indexInfo->ii_IsIndirect)
+		{
+			int		j;
+			bool	may_skip_insertion = true;
+
+			for (j = 0; j < indexInfo->ii_NumIndexAttrs; j++)
+			{
+				if (bms_is_member(indexInfo->ii_KeyAttrNumbers[j], unchangedAttrs))
+					continue;
+				may_skip_insertion = false;
+			}
+
+			/* may skip insertion if no indexed attribute changed value */
+			if (may_skip_insertion)
+				continue;
+		}
+
+		/*
 		 * FormIndexDatum fills in its values and isnull parameters with the
 		 * appropriate values for the column(s) of the index.
 		 */
@@ -357,6 +380,11 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 					   values,
 					   isnull);
 
+		/* If this is the primary key, save the values for later */
+		if (i == 0)
+			/* XXX probably need datumCopy here instead */
+			memcpy(pkeyValues, values, sizeof(Datum) * INDEX_MAX_KEYS);
+
 		/* Check whether to apply noDupErr to this index */
 		applyNoDupErr = noDupErr &&
 			(arbiterIndexes == NIL ||
@@ -389,6 +417,7 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 			index_insert(indexRelation, /* index relation */
 						 values,	/* array of index Datums */
 						 isnull,	/* null flags */
+						 pkeyValues,	/* values of primary key */
 						 tupleid,		/* tid of heap tuple */
 						 heapRelation,	/* heap relation */
 						 checkUnique);	/* type of uniqueness check to do */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index efb0c5e..2503711 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -449,7 +449,8 @@ ExecInsert(ModifyTableState *mtstate,
 			/* insert index entries for tuple */
 			recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
 												 estate, true, &specConflict,
-												   arbiterIndexes);
+												   arbiterIndexes,
+												   NULL);
 
 			/* adjust the tuple's state accordingly */
 			if (!specConflict)
@@ -495,7 +496,8 @@ ExecInsert(ModifyTableState *mtstate,
 			if (resultRelInfo->ri_NumIndices > 0)
 				recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
 													   estate, false, NULL,
-													   arbiterIndexes);
+													   arbiterIndexes,
+													   NULL);
 		}
 	}
 
@@ -895,6 +897,7 @@ ExecUpdate(ItemPointer tupleid,
 	else
 	{
 		LockTupleMode lockmode;
+		Bitmapset  *unchangedAttrs = NULL;
 
 		/*
 		 * Constraints might reference the tableoid column, so initialize
@@ -938,7 +941,9 @@ lreplace:;
 							 estate->es_output_cid,
 							 estate->es_crosscheck_snapshot,
 							 true /* wait for commit */ ,
-							 &hufd, &lockmode);
+							 &hufd,
+							 &unchangedAttrs,
+							 &lockmode);
 		switch (result)
 		{
 			case HeapTupleSelfUpdated:
@@ -1028,7 +1033,8 @@ lreplace:;
 		 */
 		if (resultRelInfo->ri_NumIndices > 0 && !HeapTupleIsHeapOnly(tuple))
 			recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
-												   estate, false, NULL, NIL);
+												   estate, false, NULL, NIL,
+												   unchangedAttrs);
 	}
 
 	if (canSetTag)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 71714bc..9880321 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3145,6 +3145,7 @@ _copyIndexStmt(const IndexStmt *from)
 	COPY_SCALAR_FIELD(indexOid);
 	COPY_SCALAR_FIELD(oldNode);
 	COPY_SCALAR_FIELD(unique);
+	COPY_SCALAR_FIELD(isindirect);
 	COPY_SCALAR_FIELD(primary);
 	COPY_SCALAR_FIELD(isconstraint);
 	COPY_SCALAR_FIELD(deferrable);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 29a090f..b91d924 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1271,6 +1271,7 @@ _equalIndexStmt(const IndexStmt *a, const IndexStmt *b)
 	COMPARE_SCALAR_FIELD(indexOid);
 	COMPARE_SCALAR_FIELD(oldNode);
 	COMPARE_SCALAR_FIELD(unique);
+	COMPARE_SCALAR_FIELD(isindirect);
 	COMPARE_SCALAR_FIELD(primary);
 	COMPARE_SCALAR_FIELD(isconstraint);
 	COMPARE_SCALAR_FIELD(deferrable);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index ae86954..4f3b864 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2449,6 +2449,7 @@ _outIndexStmt(StringInfo str, const IndexStmt *node)
 	WRITE_OID_FIELD(indexOid);
 	WRITE_OID_FIELD(oldNode);
 	WRITE_BOOL_FIELD(unique);
+	WRITE_BOOL_FIELD(isindirect);
 	WRITE_BOOL_FIELD(primary);
 	WRITE_BOOL_FIELD(isconstraint);
 	WRITE_BOOL_FIELD(deferrable);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5547fc8..cecd6d4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -410,7 +410,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	overlay_placing substr_from substr_for
 
 %type <boolean> opt_instead
-%type <boolean> opt_unique opt_concurrently opt_verbose opt_full
+%type <boolean> opt_unique opt_indirect opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_default opt_recheck
 %type <defelt>	opt_binary opt_oids copy_delimiter
 
@@ -596,7 +596,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
 	IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P
-	INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
+	INCLUDING INCREMENT INDEX INDEXES INDIRECT
+	INHERIT INHERITS INITIALLY INLINE_P
 	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
 	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
 
@@ -6651,25 +6652,26 @@ defacl_privilege_target:
  * willing to make TABLESPACE a fully reserved word.
  *****************************************************************************/
 
-IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
+IndexStmt:	CREATE opt_unique opt_indirect INDEX opt_concurrently opt_index_name
 			ON qualified_name access_method_clause '(' index_params ')'
 			opt_reloptions OptTableSpace where_clause
 				{
 					IndexStmt *n = makeNode(IndexStmt);
 					n->unique = $2;
-					n->concurrent = $4;
-					n->idxname = $5;
-					n->relation = $7;
-					n->accessMethod = $8;
-					n->indexParams = $10;
-					n->options = $12;
-					n->tableSpace = $13;
-					n->whereClause = $14;
+					n->concurrent = $5;
+					n->idxname = $6;
+					n->relation = $8;
+					n->accessMethod = $9;
+					n->indexParams = $11;
+					n->options = $13;
+					n->tableSpace = $14;
+					n->whereClause = $15;
 					n->excludeOpNames = NIL;
 					n->idxcomment = NULL;
 					n->indexOid = InvalidOid;
 					n->oldNode = InvalidOid;
 					n->primary = false;
+					n->isindirect = $3;
 					n->isconstraint = false;
 					n->deferrable = false;
 					n->initdeferred = false;
@@ -6677,25 +6679,26 @@ IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
 					n->if_not_exists = false;
 					$$ = (Node *)n;
 				}
-			| CREATE opt_unique INDEX opt_concurrently IF_P NOT EXISTS index_name
+			| CREATE opt_unique opt_indirect INDEX opt_concurrently IF_P NOT EXISTS index_name
 			ON qualified_name access_method_clause '(' index_params ')'
 			opt_reloptions OptTableSpace where_clause
 				{
 					IndexStmt *n = makeNode(IndexStmt);
 					n->unique = $2;
-					n->concurrent = $4;
-					n->idxname = $8;
-					n->relation = $10;
-					n->accessMethod = $11;
-					n->indexParams = $13;
-					n->options = $15;
-					n->tableSpace = $16;
-					n->whereClause = $17;
+					n->concurrent = $5;
+					n->idxname = $9;
+					n->relation = $11;
+					n->accessMethod = $12;
+					n->indexParams = $14;
+					n->options = $16;
+					n->tableSpace = $17;
+					n->whereClause = $18;
 					n->excludeOpNames = NIL;
 					n->idxcomment = NULL;
 					n->indexOid = InvalidOid;
 					n->oldNode = InvalidOid;
 					n->primary = false;
+					n->isindirect = $3;
 					n->isconstraint = false;
 					n->deferrable = false;
 					n->initdeferred = false;
@@ -6710,6 +6713,11 @@ opt_unique:
 			| /*EMPTY*/								{ $$ = FALSE; }
 		;
 
+opt_indirect:
+			INDIRECT								{ $$ = TRUE; }
+			| /*EMPTY*/								{ $$ = FALSE; }
+		;
+
 opt_concurrently:
 			CONCURRENTLY							{ $$ = TRUE; }
 			| /*EMPTY*/								{ $$ = FALSE; }
@@ -13775,6 +13783,7 @@ unreserved_keyword:
 			| INCREMENT
 			| INDEX
 			| INDEXES
+			| INDIRECT
 			| INHERIT
 			| INHERITS
 			| INLINE_P
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 79e0b1f..939ea34 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -266,7 +266,7 @@ static TupleDesc GetPgIndexDescriptor(void);
 static void AttrDefaultFetch(Relation relation);
 static void CheckConstraintFetch(Relation relation);
 static int	CheckConstraintCmp(const void *a, const void *b);
-static List *insert_ordered_oid(List *list, Oid datum);
+static List *insert_ordered_oid(List *list, Oid datum, bool must_be_first);
 static void InitIndexAmRoutine(Relation relation);
 static void IndexSupportInitialize(oidvector *indclass,
 					   RegProcedure *indexSupport,
@@ -2032,6 +2032,7 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc)
 	bms_free(relation->rd_indexattr);
 	bms_free(relation->rd_keyattr);
 	bms_free(relation->rd_idattr);
+	bms_free(relation->rd_indirectattr);
 	if (relation->rd_options)
 		pfree(relation->rd_options);
 	if (relation->rd_indextuple)
@@ -3942,6 +3943,47 @@ RelationGetFKeyList(Relation relation)
 }
 
 /*
+ * Return the relation's primary key OID.
+ *
+ * Surely this can be made better ...
+ */
+Oid
+RelationGetPrimaryKey(Relation relation)
+{
+	Relation	indrel;
+	SysScanDesc indscan;
+	ScanKeyData skey;
+	HeapTuple	htup;
+	Oid			pkid = InvalidOid;
+
+	/* Currently we just scan pg_index every time this is called */
+	ScanKeyInit(&skey,
+				Anum_pg_index_indrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+
+	indrel = heap_open(IndexRelationId, AccessShareLock);
+	indscan = systable_beginscan(indrel, IndexIndrelidIndexId, true,
+								 NULL, 1, &skey);
+	while (HeapTupleIsValid(htup = systable_getnext(indscan)))
+	{
+		Form_pg_index index = (Form_pg_index) GETSTRUCT(htup);
+
+		if (!IndexIsLive(index))
+			continue;
+		if (!index->indisprimary)
+			continue;
+		pkid = index->indexrelid;
+		break;
+	}
+
+	systable_endscan(indscan);
+	heap_close(indrel, AccessShareLock);
+
+	return pkid;
+}
+
+/*
  * RelationGetIndexList -- get a list of OIDs of indexes on this relation
  *
  * The index list is created only if someone requests it.  We scan pg_index
@@ -3955,7 +3997,8 @@ RelationGetFKeyList(Relation relation)
  * Such indexes are expected to be dropped momentarily, and should not be
  * touched at all by any caller of this function.
  *
- * The returned list is guaranteed to be sorted in order by OID.  This is
+ * The returned list is guaranteed to be sorted in order by OID, except that
+ * the primary key is always in front.  This is
  * needed by the executor, since for index types that we obtain exclusive
  * locks on when updating the index, all backends must lock the indexes in
  * the same order or we will get deadlocks (see ExecOpenIndices()).  Any
@@ -4031,7 +4074,8 @@ RelationGetIndexList(Relation relation)
 			continue;
 
 		/* Add index's OID to result list in the proper order */
-		result = insert_ordered_oid(result, index->indexrelid);
+		result = insert_ordered_oid(result, index->indexrelid,
+									index->indisprimary);
 
 		/*
 		 * indclass cannot be referenced directly through the C struct,
@@ -4104,12 +4148,12 @@ RelationGetIndexList(Relation relation)
  * indexes...
  */
 static List *
-insert_ordered_oid(List *list, Oid datum)
+insert_ordered_oid(List *list, Oid datum, bool must_be_first)
 {
 	ListCell   *prev;
 
 	/* Does the datum belong at the front? */
-	if (list == NIL || datum < linitial_oid(list))
+	if (list == NIL || datum < linitial_oid(list) || must_be_first)
 		return lcons_oid(datum, list);
 	/* No, so find the entry it belongs after */
 	prev = list_head(list);
@@ -4146,7 +4190,7 @@ insert_ordered_oid(List *list, Oid datum)
  * to ensure that a correct rd_indexattr set has been cached before first
  * calling RelationSetIndexList; else a subsequent inquiry might cause a
  * wrong rd_indexattr set to get computed and cached.  Likewise, we do not
- * touch rd_keyattr or rd_idattr.
+ * touch rd_keyattr, rd_indirectattr or rd_idattr.
  */
 void
 RelationSetIndexList(Relation relation, List *indexIds, Oid oidIndex)
@@ -4375,6 +4419,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	Bitmapset  *indexattrs;		/* indexed columns */
 	Bitmapset  *uindexattrs;	/* columns in unique indexes */
 	Bitmapset  *idindexattrs;	/* columns in the replica identity */
+	Bitmapset  *indirectattrs;	/* columns in indirect indexes */
 	List	   *indexoidlist;
 	Oid			relreplindex;
 	ListCell   *l;
@@ -4391,6 +4436,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 				return bms_copy(relation->rd_keyattr);
 			case INDEX_ATTR_BITMAP_IDENTITY_KEY:
 				return bms_copy(relation->rd_idattr);
+			case INDEX_ATTR_BITMAP_INDIRECT_INDEXES:
+				return bms_copy(relation->rd_indirectattr);
 			default:
 				elog(ERROR, "unknown attrKind %u", attrKind);
 		}
@@ -4419,7 +4466,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	relreplindex = relation->rd_replidindex;
 
 	/*
-	 * For each index, add referenced attributes to indexattrs.
+	 * For each index, add referenced attributes to the attribute bitmaps.
 	 *
 	 * Note: we consider all indexes returned by RelationGetIndexList, even if
 	 * they are not indisready or indisvalid.  This is important because an
@@ -4431,6 +4478,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	indexattrs = NULL;
 	uindexattrs = NULL;
 	idindexattrs = NULL;
+	indirectattrs = NULL;
 	foreach(l, indexoidlist)
 	{
 		Oid			indexOid = lfirst_oid(l);
@@ -4439,6 +4487,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 		int			i;
 		bool		isKey;		/* candidate key */
 		bool		isIDKey;	/* replica identity index */
+		bool		isIndirect;	/* an indirect index */
+		Bitmapset  *exprattrs = NULL;
 
 		indexDesc = index_open(indexOid, AccessShareLock);
 
@@ -4453,6 +4503,9 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 		/* Is this index the configured (or default) replica identity? */
 		isIDKey = (indexOid == relreplindex);
 
+		/* Is this an indirect index? */
+		isIndirect = indexInfo->ii_IsIndirect;
+
 		/* Collect simple attribute references */
 		for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
 		{
@@ -4460,8 +4513,10 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 
 			if (attrnum != 0)
 			{
-				indexattrs = bms_add_member(indexattrs,
-							   attrnum - FirstLowInvalidHeapAttributeNumber);
+				/* FIXME the name of this is now a lie. Must fix! */
+				if (!isIndirect)
+					indexattrs = bms_add_member(indexattrs,
+												attrnum - FirstLowInvalidHeapAttributeNumber);
 
 				if (isKey)
 					uindexattrs = bms_add_member(uindexattrs,
@@ -4470,11 +4525,18 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 				if (isIDKey)
 					idindexattrs = bms_add_member(idindexattrs,
 							   attrnum - FirstLowInvalidHeapAttributeNumber);
+
+				if (isIndirect)
+					indirectattrs = bms_add_member(indirectattrs,
+							   attrnum - FirstLowInvalidHeapAttributeNumber);
 			}
 		}
 
 		/* Collect all attributes used in expressions, too */
-		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
+		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &exprattrs);
+		indexattrs = bms_add_members(indexattrs, exprattrs);
+		indirectattrs = bms_add_members(indirectattrs, exprattrs);
+		bms_free(exprattrs);
 
 		/* Collect all attributes in the index predicate, too */
 		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
@@ -4491,6 +4553,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	relation->rd_keyattr = NULL;
 	bms_free(relation->rd_idattr);
 	relation->rd_idattr = NULL;
+	bms_free(relation->rd_indirectattr);
+	relation->rd_indirectattr = NULL;
 
 	/*
 	 * Now save copies of the bitmaps in the relcache entry.  We intentionally
@@ -4503,6 +4567,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	relation->rd_keyattr = bms_copy(uindexattrs);
 	relation->rd_idattr = bms_copy(idindexattrs);
 	relation->rd_indexattr = bms_copy(indexattrs);
+	relation->rd_indirectattr = bms_copy(indirectattrs);
 	MemoryContextSwitchTo(oldcxt);
 
 	/* We return our original working copy for caller to play with */
@@ -4514,6 +4579,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 			return uindexattrs;
 		case INDEX_ATTR_BITMAP_IDENTITY_KEY:
 			return idindexattrs;
+		case INDEX_ATTR_BITMAP_INDIRECT_INDEXES:
+			return indirectattrs;
 		default:
 			elog(ERROR, "unknown attrKind %u", attrKind);
 			return NULL;
@@ -5059,6 +5126,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_indexattr = NULL;
 		rel->rd_keyattr = NULL;
 		rel->rd_idattr = NULL;
+		rel->rd_indirectattr = NULL;
 		rel->rd_createSubid = InvalidSubTransactionId;
 		rel->rd_newRelfilenodeSubid = InvalidSubTransactionId;
 		rel->rd_amcache = NULL;
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 1036cca..9f28c7d 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -175,6 +175,8 @@ typedef struct IndexAmRoutine
 	bool		amclusterable;
 	/* does AM handle predicate locks? */
 	bool		ampredlocks;
+	/* does AM support indirect indexes? */
+	bool		amcanindirect;
 	/* type of data stored in index, or InvalidOid if variable */
 	Oid			amkeytype;
 
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 81907d5..c247b89 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -102,13 +102,17 @@ typedef struct SysScanDescData *SysScanDesc;
  * call is made with UNIQUE_CHECK_EXISTING.  The tuple is already in the
  * index in this case, so it should not be inserted again.  Rather, just
  * check for conflicting live tuples (possibly blocking).
+ *
+ * UNIQUE_CHECK_INSERT_SINGLETON only inserts if there isn't already an
+ * index tuple.   This is supported for indirect indexes.
  */
 typedef enum IndexUniqueCheck
 {
 	UNIQUE_CHECK_NO,			/* Don't do any uniqueness checking */
 	UNIQUE_CHECK_YES,			/* Enforce uniqueness at insertion time */
 	UNIQUE_CHECK_PARTIAL,		/* Test uniqueness, but no error */
-	UNIQUE_CHECK_EXISTING		/* Check if existing tuple is unique */
+	UNIQUE_CHECK_EXISTING,		/* Check if existing tuple is unique */
+	UNIQUE_CHECK_INSERT_SINGLETON	/* Only insert if value doesn't exist */
 } IndexUniqueCheck;
 
 
@@ -127,6 +131,7 @@ extern void index_close(Relation relation, LOCKMODE lockmode);
 
 extern bool index_insert(Relation indexRelation,
 			 Datum *values, bool *isnull,
+			 Datum *pkeyValues,
 			 ItemPointer heap_t_ctid,
 			 Relation heapRelation,
 			 IndexUniqueCheck checkUnique);
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 0d12bbb..99af368 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -16,6 +16,7 @@
 
 #include "access/sdir.h"
 #include "access/skey.h"
+#include "nodes/bitmapset.h"
 #include "nodes/lockoptions.h"
 #include "nodes/primnodes.h"
 #include "storage/bufpage.h"
@@ -160,7 +161,8 @@ extern void heap_abort_speculative(Relation relation, HeapTuple tuple);
 extern HTSU_Result heap_update(Relation relation, ItemPointer otid,
 			HeapTuple newtup,
 			CommandId cid, Snapshot crosscheck, bool wait,
-			HeapUpdateFailureData *hufd, LockTupleMode *lockmode);
+			HeapUpdateFailureData *hufd, Bitmapset **unchanged_attrs,
+			LockTupleMode *lockmode);
 extern HTSU_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
 				CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy,
 				bool follow_update,
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 37e6ef3..2f21db2 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -37,6 +37,17 @@ typedef enum
 	INDEX_DROP_SET_DEAD
 } IndexStateFlagsAction;
 
+static inline void
+FAKE_CTID_FROM_PKVALUES(ItemPointer iptr, int16 pkNumKeys, Datum *pkvalues)
+{
+	/* XXX should eventually support more than one column */
+	Assert(pkNumKeys == 1);
+
+	ItemPointerSet(iptr,
+				   GET_4_BYTES(pkvalues[0]),
+				   GET_2_BYTES(pkvalues[0] << 32));
+	Assert(GET_2_BYTES(pkvalues[0] << 48) == 0);
+}
 
 extern void index_check_primary_key(Relation heapRel,
 						IndexInfo *indexInfo,
@@ -55,6 +66,7 @@ extern Oid index_create(Relation heapRelation,
 			 int16 *coloptions,
 			 Datum reloptions,
 			 bool isprimary,
+			 bool isindirect,
 			 bool isconstraint,
 			 bool deferrable,
 			 bool initdeferred,
diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h
index ee97c5d..144d204 100644
--- a/src/include/catalog/pg_index.h
+++ b/src/include/catalog/pg_index.h
@@ -34,6 +34,7 @@ CATALOG(pg_index,2610) BKI_WITHOUT_OIDS BKI_SCHEMA_MACRO
 	Oid			indrelid;		/* OID of the relation it indexes */
 	int16		indnatts;		/* number of columns in index */
 	bool		indisunique;	/* is this a unique index? */
+	bool		indisindirect;	/* is this an indirect index? */
 	bool		indisprimary;	/* is this index for primary key? */
 	bool		indisexclusion; /* is this index for exclusion constraint? */
 	bool		indimmediate;	/* is uniqueness enforced immediately? */
@@ -70,26 +71,27 @@ typedef FormData_pg_index *Form_pg_index;
  *		compiler constants for pg_index
  * ----------------
  */
-#define Natts_pg_index					19
+#define Natts_pg_index					20
 #define Anum_pg_index_indexrelid		1
 #define Anum_pg_index_indrelid			2
 #define Anum_pg_index_indnatts			3
 #define Anum_pg_index_indisunique		4
-#define Anum_pg_index_indisprimary		5
-#define Anum_pg_index_indisexclusion	6
-#define Anum_pg_index_indimmediate		7
-#define Anum_pg_index_indisclustered	8
-#define Anum_pg_index_indisvalid		9
-#define Anum_pg_index_indcheckxmin		10
-#define Anum_pg_index_indisready		11
-#define Anum_pg_index_indislive			12
-#define Anum_pg_index_indisreplident	13
-#define Anum_pg_index_indkey			14
-#define Anum_pg_index_indcollation		15
-#define Anum_pg_index_indclass			16
-#define Anum_pg_index_indoption			17
-#define Anum_pg_index_indexprs			18
-#define Anum_pg_index_indpred			19
+#define Anum_pg_index_indisindirect		5
+#define Anum_pg_index_indisprimary		6
+#define Anum_pg_index_indisexclusion	7
+#define Anum_pg_index_indimmediate		8
+#define Anum_pg_index_indisclustered	9
+#define Anum_pg_index_indisvalid		10
+#define Anum_pg_index_indcheckxmin		11
+#define Anum_pg_index_indisready		12
+#define Anum_pg_index_indislive			13
+#define Anum_pg_index_indisreplident	14
+#define Anum_pg_index_indkey			15
+#define Anum_pg_index_indcollation		16
+#define Anum_pg_index_indclass			17
+#define Anum_pg_index_indoption			18
+#define Anum_pg_index_indexprs			19
+#define Anum_pg_index_indpred			20
 
 /*
  * Index AMs that support ordered scans must support these two indoption
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 136276b..39230c1 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -367,7 +367,7 @@ extern void ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative);
 extern void ExecCloseIndices(ResultRelInfo *resultRelInfo);
 extern List *ExecInsertIndexTuples(TupleTableSlot *slot, ItemPointer tupleid,
 					  EState *estate, bool noDupErr, bool *specConflict,
-					  List *arbiterIndexes);
+					  List *arbiterIndexes, Bitmapset *unchangedAttrs);
 extern bool ExecCheckIndexConstraints(TupleTableSlot *slot, EState *estate,
 						  ItemPointer conflictTid, List *arbiterIndexes);
 extern void check_exclusion_constraint(Relation heap, Relation index,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f6f73f3..f6c4655 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -48,6 +48,7 @@
  *		UniqueProcs
  *		UniqueStrats
  *		Unique				is it a unique index?
+ *		IsIndirect			is it an indirect index?
  *		ReadyForInserts		is it valid for inserts?
  *		Concurrent			are we doing a concurrent index build?
  *		BrokenHotChain		did we detect any broken HOT chains?
@@ -72,6 +73,7 @@ typedef struct IndexInfo
 	Oid		   *ii_UniqueProcs; /* array with one entry per column */
 	uint16	   *ii_UniqueStrats;	/* array with one entry per column */
 	bool		ii_Unique;
+	bool		ii_IsIndirect;
 	bool		ii_ReadyForInserts;
 	bool		ii_Concurrent;
 	bool		ii_BrokenHotChain;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 6de2cab..e4ee0af 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2449,6 +2449,7 @@ typedef struct IndexStmt
 	Oid			indexOid;		/* OID of an existing index, if any */
 	Oid			oldNode;		/* relfilenode of existing storage, if any */
 	bool		unique;			/* is index unique? */
+	bool		isindirect;		/* is index indirect? */
 	bool		primary;		/* is index a primary key? */
 	bool		isconstraint;	/* is it for a pkey/unique constraint? */
 	bool		deferrable;		/* is the constraint DEFERRABLE? */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 17ffef5..04b7221 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -194,6 +194,7 @@ PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD)
 PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD)
 PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD)
 PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD)
+PG_KEYWORD("indirect", INDIRECT, UNRESERVED_KEYWORD)
 PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD)
 PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD)
 PG_KEYWORD("initially", INITIALLY, RESERVED_KEYWORD)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index ed14442..71c4af7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -103,6 +103,7 @@ typedef struct RelationData
 	Bitmapset  *rd_indexattr;	/* identifies columns used in indexes */
 	Bitmapset  *rd_keyattr;		/* cols that can be ref'd by foreign keys */
 	Bitmapset  *rd_idattr;		/* included in replica identity index */
+	Bitmapset  *rd_indirectattr; /* cols part of any indirect index */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index 6ea7dd2..a0dbb23 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -39,6 +39,7 @@ extern void RelationClose(Relation relation);
  */
 extern List *RelationGetFKeyList(Relation relation);
 extern List *RelationGetIndexList(Relation relation);
+extern Oid	RelationGetPrimaryKey(Relation relation);
 extern Oid	RelationGetOidIndex(Relation relation);
 extern Oid	RelationGetReplicaIndex(Relation relation);
 extern List *RelationGetIndexExpressions(Relation relation);
@@ -48,7 +49,8 @@ typedef enum IndexAttrBitmapKind
 {
 	INDEX_ATTR_BITMAP_ALL,
 	INDEX_ATTR_BITMAP_KEY,
-	INDEX_ATTR_BITMAP_IDENTITY_KEY
+	INDEX_ATTR_BITMAP_IDENTITY_KEY,
+	INDEX_ATTR_BITMAP_INDIRECT_INDEXES
 } IndexAttrBitmapKind;
 
 extern Bitmapset *RelationGetIndexAttrBitmap(Relation relation,

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Attachments:

  [text/plain] indirect.patch (55.7K, ../20161101044331.4w73ywvi42niltva@alvherre.pgsql/2-indirect.patch)
  download | inline diff:
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1b45a4c..9f899c7 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -92,6 +92,7 @@ brinhandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = true;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = brinbuild;
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index f07eedc..1bc91d2 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -49,6 +49,7 @@ ginhandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = true;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = ginbuild;
diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c
index b8aa9bc..4ec34d5 100644
--- a/src/backend/access/gist/gist.c
+++ b/src/backend/access/gist/gist.c
@@ -69,6 +69,7 @@ gisthandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = true;
 	amroutine->amclusterable = true;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = gistbuild;
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index e3b1eef..3fa3d71 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -66,6 +66,7 @@ hashhandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = false;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = INT4OID;
 
 	amroutine->ambuild = hashbuild;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index b019bc1..9e91b41 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -96,10 +96,10 @@ static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf,
 				HeapTuple newtup, HeapTuple old_key_tup,
 				bool all_visible_cleared, bool new_all_visible_cleared);
 static void HeapSatisfiesHOTandKeyUpdate(Relation relation,
-							 Bitmapset *hot_attrs,
-							 Bitmapset *key_attrs, Bitmapset *id_attrs,
+							 Bitmapset *hot_attrs, Bitmapset *key_attrs,
+							 Bitmapset *id_attrs, Bitmapset *indirect_attrs,
 							 bool *satisfies_hot, bool *satisfies_key,
-							 bool *satisfies_id,
+							 bool *satisfies_id, Bitmapset **unchanged_attrs,
 							 HeapTuple oldtup, HeapTuple newtup);
 static bool heap_acquire_tuplock(Relation relation, ItemPointer tid,
 					 LockTupleMode mode, LockWaitPolicy wait_policy,
@@ -3414,6 +3414,8 @@ simple_heap_delete(Relation relation, ItemPointer tid)
  *	crosscheck - if not InvalidSnapshot, also check old tuple against this
  *	wait - true if should wait for any conflicting update to commit/abort
  *	hufd - output parameter, filled in failure cases (see below)
+ *	unchanged_ind_cols - output parameter; bits set for unmodified columns
+ *		that are indexed by indirect indexes
  *	lockmode - output parameter, filled with lock mode acquired on tuple
  *
  * Normal, successful return value is HeapTupleMayBeUpdated, which
@@ -3436,13 +3438,15 @@ simple_heap_delete(Relation relation, ItemPointer tid)
 HTSU_Result
 heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 			CommandId cid, Snapshot crosscheck, bool wait,
-			HeapUpdateFailureData *hufd, LockTupleMode *lockmode)
+			HeapUpdateFailureData *hufd, Bitmapset **unchanged_ind_cols,
+			LockTupleMode *lockmode)
 {
 	HTSU_Result result;
 	TransactionId xid = GetCurrentTransactionId();
 	Bitmapset  *hot_attrs;
 	Bitmapset  *key_attrs;
 	Bitmapset  *id_attrs;
+	Bitmapset  *indirect_attrs;
 	ItemId		lp;
 	HeapTupleData oldtup;
 	HeapTuple	heaptup;
@@ -3504,6 +3508,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY);
 	id_attrs = RelationGetIndexAttrBitmap(relation,
 										  INDEX_ATTR_BITMAP_IDENTITY_KEY);
+	indirect_attrs = RelationGetIndexAttrBitmap(relation,
+												INDEX_ATTR_BITMAP_INDIRECT_INDEXES);
 
 	block = ItemPointerGetBlockNumber(otid);
 	buffer = ReadBuffer(relation, block);
@@ -3561,9 +3567,11 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 	 * is updates that don't manipulate key columns, not those that
 	 * serendipitiously arrive at the same key values.
 	 */
-	HeapSatisfiesHOTandKeyUpdate(relation, hot_attrs, key_attrs, id_attrs,
+	HeapSatisfiesHOTandKeyUpdate(relation,
+								 hot_attrs, key_attrs, id_attrs, indirect_attrs,
 								 &satisfies_hot, &satisfies_key,
-								 &satisfies_id, &oldtup, newtup);
+								 &satisfies_id, unchanged_ind_cols,
+								 &oldtup, newtup);
 	if (satisfies_key)
 	{
 		*lockmode = LockTupleNoKeyExclusive;
@@ -3803,6 +3811,7 @@ l2:
 		bms_free(hot_attrs);
 		bms_free(key_attrs);
 		bms_free(id_attrs);
+		bms_free(indirect_attrs);
 		return result;
 	}
 
@@ -4356,41 +4365,53 @@ heap_tuple_attr_equals(TupleDesc tupdesc, int attrnum,
  * Check which columns are being updated.
  *
  * This simultaneously checks conditions for HOT updates, for FOR KEY
- * SHARE updates, and REPLICA IDENTITY concerns.  Since much of the time they
- * will be checking very similar sets of columns, and doing the same tests on
- * them, it makes sense to optimize and do them together.
+ * SHARE updates, for REPLICA IDENTITY concerns, and for indirect indexing
+ * concerns.  Since much of the time they will be checking very similar sets of
+ * columns, and doing the same tests on them, it makes sense to optimize and do
+ * them together.
  *
- * We receive three bitmapsets comprising the three sets of columns we're
+ * We receive four bitmapsets comprising the four sets of columns we're
  * interested in.  Note these are destructively modified; that is OK since
  * this is invoked at most once in heap_update.
  *
- * hot_result is set to TRUE if it's okay to do a HOT update (i.e. it does not
- * modified indexed columns); key_result is set to TRUE if the update does not
- * modify columns used in the key; id_result is set to TRUE if the update does
- * not modify columns in any index marked as the REPLICA IDENTITY.
+ * satisfies_hot is set to TRUE if it's okay to do a HOT update (i.e. it does
+ * not modified indexed columns); satisfies_key is set to TRUE if the update
+ * does not modify columns used in the key; satisfies_id is set to TRUE if the
+ * update does not modify columns in any index marked as the REPLICA IDENTITY.
+ *
+ * unchanged_attrs is an output bitmapset that has a bit set if the
+ * corresponding column is indexed by an indirect index and is not modified.
+ * Note that because system columns cannot be indexed by indirect indexes,
+ * these values are not shifted by FirstLowInvalidHeapAttributeNumber.
  */
 static void
 HeapSatisfiesHOTandKeyUpdate(Relation relation, Bitmapset *hot_attrs,
 							 Bitmapset *key_attrs, Bitmapset *id_attrs,
+							 Bitmapset *indirect_attrs,
 							 bool *satisfies_hot, bool *satisfies_key,
-							 bool *satisfies_id,
+							 bool *satisfies_id, Bitmapset **unchanged_attrs,
 							 HeapTuple oldtup, HeapTuple newtup)
 {
 	int			next_hot_attnum;
 	int			next_key_attnum;
 	int			next_id_attnum;
+	int			next_indirect_attnum;
 	bool		hot_result = true;
 	bool		key_result = true;
 	bool		id_result = true;
+	bool		collecting_indirect;
 
 	/* If REPLICA IDENTITY is set to FULL, id_attrs will be empty. */
 	Assert(bms_is_subset(id_attrs, key_attrs));
-	Assert(bms_is_subset(key_attrs, hot_attrs));
+
+	/* Waste no time on indirect indexes if there aren't any */
+	collecting_indirect = (unchanged_attrs != NULL) &&
+		!bms_is_empty(indirect_attrs);
 
 	/*
 	 * If one of these sets contains no remaining bits, bms_first_member will
 	 * return -1, and after adding FirstLowInvalidHeapAttributeNumber (which
-	 * is negative!)  we'll get an attribute number that can't possibly be
+	 * is negative!) we'll get an attribute number that can't possibly be
 	 * real, and thus won't match any actual attribute number.
 	 */
 	next_hot_attnum = bms_first_member(hot_attrs);
@@ -4399,24 +4420,43 @@ HeapSatisfiesHOTandKeyUpdate(Relation relation, Bitmapset *hot_attrs,
 	next_key_attnum += FirstLowInvalidHeapAttributeNumber;
 	next_id_attnum = bms_first_member(id_attrs);
 	next_id_attnum += FirstLowInvalidHeapAttributeNumber;
+	next_indirect_attnum = bms_first_member(indirect_attrs);
+	next_indirect_attnum += FirstLowInvalidHeapAttributeNumber;
 
 	for (;;)
 	{
 		bool		changed;
 		int			check_now;
 
+		/* Set to a value greater than what can be attained normally */
+		check_now = MaxAttrNumber + 1;
+
 		/*
-		 * Since the HOT attributes are a superset of the key attributes and
-		 * the key attributes are a superset of the id attributes, this logic
-		 * is guaranteed to identify the next column that needs to be checked.
+		 * Since the key attributes are a superset of the id attributes, this
+		 * logic is guaranteed to identify the next column that needs to be
+		 * checked.  We consider indirect attributes and HOT attributes
+		 * separately because they aren't supersets of anything.
 		 */
-		if (hot_result && next_hot_attnum > FirstLowInvalidHeapAttributeNumber)
-			check_now = next_hot_attnum;
-		else if (key_result && next_key_attnum > FirstLowInvalidHeapAttributeNumber)
+		if (key_result && next_key_attnum > FirstLowInvalidHeapAttributeNumber)
 			check_now = next_key_attnum;
 		else if (id_result && next_id_attnum > FirstLowInvalidHeapAttributeNumber)
 			check_now = next_id_attnum;
-		else
+
+		if (hot_result && next_hot_attnum > FirstLowInvalidHeapAttributeNumber)
+		{
+			if (next_hot_attnum < check_now)
+				check_now = next_hot_attnum;
+		}
+
+		if (collecting_indirect)
+		{
+			Assert(next_indirect_attnum > 0);
+			if (next_indirect_attnum < check_now)
+				check_now = next_indirect_attnum;
+		}
+
+		/* are we done? */
+		if (check_now == MaxAttrNumber + 1)
 			break;
 
 		/* See whether it changed. */
@@ -4430,13 +4470,33 @@ HeapSatisfiesHOTandKeyUpdate(Relation relation, Bitmapset *hot_attrs,
 				key_result = false;
 			if (check_now == next_id_attnum)
 				id_result = false;
-
-			/* if all are false now, we can stop checking */
-			if (!hot_result && !key_result && !id_result)
-				break;
 		}
 
 		/*
+		 * Deal with indirect indexes.  Set the bit in the output set if the
+		 * column did not change, and compute the next column, which is
+		 * necessary to determine whether we need to iterate again.
+		 */
+		if (collecting_indirect && check_now == next_indirect_attnum)
+		{
+			if (!changed)
+				*unchanged_attrs = bms_add_member(*unchanged_attrs,
+												  check_now);
+			next_indirect_attnum = bms_first_member(indirect_attrs);
+			/* are we done with these? */
+			if (next_indirect_attnum == -1)
+				collecting_indirect = false;
+			next_indirect_attnum += FirstLowInvalidHeapAttributeNumber;
+		}
+
+		/*
+		 * if HOT, key, id are all false now and we're not collecting
+		 * unchanged indirect index columns, we can stop checking.
+		 */
+		if (!hot_result && !key_result && !id_result && !collecting_indirect)
+			break;
+
+		/*
 		 * Advance the next attribute numbers for the sets that contain the
 		 * attribute we just checked.  As we work our way through the columns,
 		 * the next_attnum values will rise; but when each set becomes empty,
@@ -4483,7 +4543,7 @@ simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup)
 	result = heap_update(relation, otid, tup,
 						 GetCurrentCommandId(true), InvalidSnapshot,
 						 true /* wait for commit */ ,
-						 &hufd, &lockmode);
+						 &hufd, NULL, &lockmode);
 	switch (result)
 	{
 		case HeapTupleSelfUpdated:
diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c
index fc4702c..07bf463 100644
--- a/src/backend/access/heap/tuptoaster.c
+++ b/src/backend/access/heap/tuptoaster.c
@@ -1614,6 +1614,7 @@ toast_save_datum(Relation rel, Datum value,
 			/* Only index relations marked as ready can be updated */
 			if (IndexIsReady(toastidxs[i]->rd_index))
 				index_insert(toastidxs[i], t_values, t_isnull,
+							 NULL,
 							 &(toasttup->t_self),
 							 toastrel,
 							 toastidxs[i]->rd_index->indisunique ?
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 54b71cb..8b114ab 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -189,10 +189,13 @@ bool
 index_insert(Relation indexRelation,
 			 Datum *values,
 			 bool *isnull,
+			 Datum *pkeyValues,
 			 ItemPointer heap_t_ctid,
 			 Relation heapRelation,
 			 IndexUniqueCheck checkUnique)
 {
+	ItemPointerData iptr;
+
 	RELATION_CHECKS;
 	CHECK_REL_PROCEDURE(aminsert);
 
@@ -201,8 +204,19 @@ index_insert(Relation indexRelation,
 									   (HeapTuple) NULL,
 									   InvalidBuffer);
 
+	/*
+	 * Indirect indexes use a fake item pointer constructed from the primary
+	 * key values; regular indexes store the actual heap item pointer.
+	 */
+	if (!indexRelation->rd_index->indisindirect)
+		ItemPointerCopy(heap_t_ctid, &iptr);
+	else
+		FAKE_CTID_FROM_PKVALUES(&iptr,
+								indexRelation->rd_index->indnatts,
+								pkeyValues);
+
 	return indexRelation->rd_amroutine->aminsert(indexRelation, values, isnull,
-												 heap_t_ctid, heapRelation,
+												 &iptr, heapRelation,
 												 checkUnique);
 }
 
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index ef69290..eb4beef 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -92,7 +92,9 @@ static void _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel);
  *		By here, itup is filled in, including the TID.
  *
  *		If checkUnique is UNIQUE_CHECK_NO or UNIQUE_CHECK_PARTIAL, this
- *		will allow duplicates.  Otherwise (UNIQUE_CHECK_YES or
+ *		will allow duplicates.  If it's UNIQUE_CHECK_INSERT_SINGLETON, the value
+ *		will only be inserted if there isn't already a tuple with that value.
+ *		Otherwise (UNIQUE_CHECK_YES or
  *		UNIQUE_CHECK_EXISTING) it will throw error for a duplicate.
  *		For UNIQUE_CHECK_EXISTING we merely run the duplicate check, and
  *		don't actually insert.
@@ -100,8 +102,8 @@ static void _bt_vacuum_one_page(Relation rel, Buffer buffer, Relation heapRel);
  *		The result value is only significant for UNIQUE_CHECK_PARTIAL:
  *		it must be TRUE if the entry is known unique, else FALSE.
  *		(In the current implementation we'll also return TRUE after a
- *		successful UNIQUE_CHECK_YES or UNIQUE_CHECK_EXISTING call, but
- *		that's just a coding artifact.)
+ *		successful UNIQUE_CHECK_YES, UNIQUE_CHECK_EXISTING or
+ *		UNIQUE_CHECK_INSERT_SINGLETON call, but that's just a coding artifact.)
  */
 bool
 _bt_doinsert(Relation rel, IndexTuple itup,
@@ -138,6 +140,21 @@ top:
 						true, stack, BT_WRITE, NULL);
 
 	/*
+	 * In insert-singleton mode, we must return without doing anything if the
+	 * value we're inserting already exists.
+	 */
+#if 0
+	if (checkUnique == UNIQUE_CHECK_INSERT_SINGLETON)
+	{
+		offset = _bt_binsrch( .. );
+		if (offset is valid and contains a tuple matching the scankey)
+			return true;
+		/* otherwise fall through to insert */
+	}
+#endif
+
+
+	/*
 	 * If we're not allowing duplicates, make sure the key isn't already in
 	 * the index.
 	 *
@@ -158,7 +175,8 @@ top:
 	 * let the tuple in and return false for possibly non-unique, or true for
 	 * definitely unique.
 	 */
-	if (checkUnique != UNIQUE_CHECK_NO)
+	if (checkUnique != UNIQUE_CHECK_NO &&
+		checkUnique != UNIQUE_CHECK_INSERT_SINGLETON)
 	{
 		TransactionId xwait;
 		uint32		speculativeToken;
@@ -167,6 +185,10 @@ top:
 		xwait = _bt_check_unique(rel, itup, heapRel, buf, offset, itup_scankey,
 								 checkUnique, &is_unique, &speculativeToken);
 
+		if (checkUnique == UNIQUE_CHECK_INSERT_SINGLETON &&
+			TransactionIdIsValid(xwait))
+			return true;
+
 		if (TransactionIdIsValid(xwait))
 		{
 			/* Have to wait for the other guy ... */
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 128744c..bbf06d2 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -37,6 +37,7 @@ typedef struct
 {
 	bool		isUnique;
 	bool		haveDead;
+	bool		isIndirect;
 	Relation	heapRel;
 	BTSpool    *spool;
 
@@ -45,6 +46,8 @@ typedef struct
 	 * put into spool2 instead of spool in order to avoid uniqueness check.
 	 */
 	BTSpool    *spool2;
+	int16		pkNumKeys;
+	AttrNumber	pkAttnums[INDEX_MAX_KEYS];
 	double		indtuples;
 } BTBuildState;
 
@@ -98,6 +101,7 @@ bthandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = false;
 	amroutine->amclusterable = true;
 	amroutine->ampredlocks = true;
+	amroutine->amcanindirect = true;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = btbuild;
@@ -136,6 +140,25 @@ btbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	buildstate.heapRel = heap;
 	buildstate.spool = NULL;
 	buildstate.spool2 = NULL;
+	buildstate.isIndirect = indexInfo->ii_IsIndirect;
+	if (indexInfo->ii_IsIndirect)
+	{
+		Oid			pkOid;
+		Relation	pkRel;
+		int			i;
+
+		pkOid = RelationGetPrimaryKey(heap);
+		pkRel = index_open(pkOid, AccessShareLock);
+
+		buildstate.pkNumKeys = pkRel->rd_index->indnatts;
+		for (i = 0; i < buildstate.pkNumKeys; i++)
+			buildstate.pkAttnums[i] = pkRel->rd_index->indkey.values[i];
+		index_close(pkRel, AccessShareLock);
+	}
+	else
+	{
+		buildstate.pkNumKeys = 0;
+	}
 	buildstate.indtuples = 0;
 
 #ifdef BTREE_BUILD_STATS
@@ -213,18 +236,42 @@ btbuildCallback(Relation index,
 				void *state)
 {
 	BTBuildState *buildstate = (BTBuildState *) state;
+	ItemPointerData iptr;
+
+	if (buildstate->isIndirect)
+	{
+		Datum	pkValues[INDEX_MAX_KEYS];
+		int		i;
+		bool	isnull;
+
+		/*
+		 * XXX WAG: this is very slow in the general case, but OK if PK column
+		 * is first.
+		 */
+		for (i = 0; i < buildstate->pkNumKeys; i++)
+		{
+			pkValues[i] = heap_getattr(htup,
+									   buildstate->pkAttnums[i],
+									   RelationGetDescr(buildstate->heapRel),
+									   &isnull);
+			Assert(!isnull);
+		}
+		FAKE_CTID_FROM_PKVALUES(&iptr, buildstate->pkNumKeys, pkValues);
+	}
+	else
+		ItemPointerCopy(&htup->t_self, &iptr);
 
 	/*
 	 * insert the index tuple into the appropriate spool file for subsequent
 	 * processing
 	 */
 	if (tupleIsAlive || buildstate->spool2 == NULL)
-		_bt_spool(buildstate->spool, &htup->t_self, values, isnull);
+		_bt_spool(buildstate->spool, &iptr, values, isnull);
 	else
 	{
 		/* dead tuples are put into spool2 */
 		buildstate->haveDead = true;
-		_bt_spool(buildstate->spool2, &htup->t_self, values, isnull);
+		_bt_spool(buildstate->spool2, &iptr, values, isnull);
 	}
 
 	buildstate->indtuples += 1;
diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c
index d570ae5..9378919 100644
--- a/src/backend/access/spgist/spgutils.c
+++ b/src/backend/access/spgist/spgutils.c
@@ -48,6 +48,7 @@ spghandler(PG_FUNCTION_ARGS)
 	amroutine->amstorage = false;
 	amroutine->amclusterable = false;
 	amroutine->ampredlocks = false;
+	amroutine->amcanindirect = false;
 	amroutine->amkeytype = InvalidOid;
 
 	amroutine->ambuild = spgbuild;
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 08b646d..1f57eb5 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -104,6 +104,7 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 					int16 *coloptions,
 					bool primary,
 					bool isexclusion,
+					bool isindirect,
 					bool immediate,
 					bool isvalid);
 static void index_update_stats(Relation rel,
@@ -554,6 +555,7 @@ UpdateIndexRelation(Oid indexoid,
 					int16 *coloptions,
 					bool primary,
 					bool isexclusion,
+					bool isindirect,
 					bool immediate,
 					bool isvalid)
 {
@@ -623,6 +625,7 @@ UpdateIndexRelation(Oid indexoid,
 	values[Anum_pg_index_indrelid - 1] = ObjectIdGetDatum(heapoid);
 	values[Anum_pg_index_indnatts - 1] = Int16GetDatum(indexInfo->ii_NumIndexAttrs);
 	values[Anum_pg_index_indisunique - 1] = BoolGetDatum(indexInfo->ii_Unique);
+	values[Anum_pg_index_indisindirect - 1] = BoolGetDatum(isindirect);
 	values[Anum_pg_index_indisprimary - 1] = BoolGetDatum(primary);
 	values[Anum_pg_index_indisexclusion - 1] = BoolGetDatum(isexclusion);
 	values[Anum_pg_index_indimmediate - 1] = BoolGetDatum(immediate);
@@ -681,6 +684,7 @@ UpdateIndexRelation(Oid indexoid,
  * coloptions: array of per-index-column indoption settings
  * reloptions: AM-specific options
  * isprimary: index is a PRIMARY KEY
+ * isindirect: index is an indirect one
  * isconstraint: index is owned by PRIMARY KEY, UNIQUE, or EXCLUSION constraint
  * deferrable: constraint is DEFERRABLE
  * initdeferred: constraint is INITIALLY DEFERRED
@@ -710,6 +714,7 @@ index_create(Relation heapRelation,
 			 int16 *coloptions,
 			 Datum reloptions,
 			 bool isprimary,
+			 bool isindirect,
 			 bool isconstraint,
 			 bool deferrable,
 			 bool initdeferred,
@@ -769,6 +774,21 @@ index_create(Relation heapRelation,
 				 errmsg("concurrent index creation on system catalog tables is not supported")));
 
 	/*
+	 * indirect indexes are forbidden on system catalogs, and they obviously cannot
+	 * be primary keys either.
+	 */
+	if (isindirect &&
+		IsSystemRelation(heapRelation))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("indirect index creation on system catalog tables is not supported")));
+	if (isindirect && isprimary)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("primary key indexes cannot be indirect")));
+	/* XXX other restrictions needed? */
+
+	/*
 	 * This case is currently not supported, but there's no way to ask for it
 	 * in the grammar anyway, so it can't happen.
 	 */
@@ -916,7 +936,7 @@ index_create(Relation heapRelation,
 	 */
 	UpdateIndexRelation(indexRelationId, heapRelationId, indexInfo,
 						collationObjectId, classObjectId, coloptions,
-						isprimary, is_exclusion,
+						isprimary, is_exclusion, isindirect,
 						!deferrable,
 						!concurrent);
 
@@ -1011,6 +1031,14 @@ index_create(Relation heapRelation,
 			Assert(!initdeferred);
 		}
 
+		/* Store dependency on primary key index, if needed */
+		if (isindirect)
+		{
+			ObjectAddressSet(referenced, RelationRelationId,
+							 RelationGetPrimaryKey(heapRelation));
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
 		/* Store dependency on collations */
 		/* The default collation is pinned, so don't bother recording it */
 		for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
@@ -1681,6 +1709,7 @@ BuildIndexInfo(Relation index)
 
 	/* other info */
 	ii->ii_Unique = indexStruct->indisunique;
+	ii->ii_IsIndirect = indexStruct->indisindirect;
 	ii->ii_ReadyForInserts = IndexIsReady(indexStruct);
 	/* assume not doing speculative insertion for now */
 	ii->ii_UniqueOps = NULL;
@@ -3161,6 +3190,7 @@ validate_index_heapscan(Relation heapRelation,
 			index_insert(indexRelation,
 						 values,
 						 isnull,
+						 NULL,	/* FIXME need to PK values here */
 						 &rootTuple,
 						 heapRelation,
 						 indexInfo->ii_Unique ?
@@ -3564,7 +3594,10 @@ reindex_relation(Oid relid, int flags, int options)
 
 	/* Ensure rd_indexattr is valid; see comments for RelationSetIndexList */
 	if (is_pg_class)
+	{
+		elog(ERROR, "FIXME must fix this case");
 		(void) RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_ALL);
+	}
 
 	PG_TRY();
 	{
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index b9fe102..f00f446 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -136,6 +136,7 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple)
 		index_insert(relationDescs[i],	/* index relation */
 					 values,	/* array of index Datums */
 					 isnull,	/* is-null flags */
+					 NULL,		/* catalogs never had indirect indexes */
 					 &(heapTuple->t_self),		/* tid of heap tuple */
 					 heapRelation,
 					 relationDescs[i]->rd_index->indisunique ?
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 564e10e..caa25ba 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -331,7 +331,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 				 BTREE_AM_OID,
 				 rel->rd_rel->reltablespace,
 				 collationObjectId, classObjectId, coloptions, (Datum) 0,
-				 true, false, false, false,
+				 true, false, false, false, false,
 				 true, false, false, true, false);
 
 	heap_close(toast_rel, NoLock);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index dc1f79f..13f28d3 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1535,14 +1535,14 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	reindex_relation(OIDOldHeap, reindex_flags, 0);
 
 	/*
-	 * If the relation being rebuild is pg_class, swap_relation_files()
+	 * If the relation being rebuilt is pg_class, swap_relation_files()
 	 * couldn't update pg_class's own pg_class entry (check comments in
 	 * swap_relation_files()), thus relfrozenxid was not updated. That's
 	 * annoying because a potential reason for doing a VACUUM FULL is a
 	 * imminent or actual anti-wraparound shutdown.  So, now that we can
-	 * access the new relation using it's indices, update relfrozenxid.
+	 * access the new relation using its indices, update relfrozenxid.
 	 * pg_class doesn't have a toast relation, so we don't need to update the
-	 * corresponding toast relation. Not that there's little point moving all
+	 * corresponding toast relation. Note that there's little point moving all
 	 * relfrozenxid updates here since swap_relation_files() needs to write to
 	 * pg_class for non-mapped relations anyway.
 	 */
diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c
index 26f9114..5f1a7b7 100644
--- a/src/backend/commands/constraint.c
+++ b/src/backend/commands/constraint.c
@@ -164,7 +164,7 @@ unique_key_recheck(PG_FUNCTION_ARGS)
 		 * correct even if t_self is now dead, because that is the TID the
 		 * index will know about.
 		 */
-		index_insert(indexRel, values, isnull, &(new_row->t_self),
+		index_insert(indexRel, values, isnull, NULL, &(new_row->t_self),
 					 trigdata->tg_relation, UNIQUE_CHECK_EXISTING);
 	}
 	else
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index b4140eb..b9e9e20 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -2535,7 +2535,7 @@ CopyFrom(CopyState cstate)
 				if (resultRelInfo->ri_NumIndices > 0)
 					recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
 														 estate, false, NULL,
-														   NIL);
+														   NIL, NULL);
 
 				/* AFTER ROW INSERT Triggers */
 				ExecARInsertTriggers(estate, resultRelInfo, tuple,
@@ -2649,7 +2649,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 			ExecStoreTuple(bufferedTuples[i], myslot, InvalidBuffer, false);
 			recheckIndexes =
 				ExecInsertIndexTuples(myslot, &(bufferedTuples[i]->t_self),
-									  estate, false, NULL, NIL);
+									  estate, false, NULL, NIL, NULL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
 								 recheckIndexes);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 85817c6..17fbf2d 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -519,6 +519,11 @@ DefineIndex(Oid relationId,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 		errmsg("access method \"%s\" does not support exclusion constraints",
 			   accessMethodName)));
+	if (stmt->isindirect && !amRoutine->amcanindirect)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("access method \"%s\" does not support indirect indexes",
+						accessMethodName)));
 
 	amcanorder = amRoutine->amcanorder;
 	amoptions = amRoutine->amoptions;
@@ -653,7 +658,7 @@ DefineIndex(Oid relationId,
 					 indexInfo, indexColNames,
 					 accessMethodId, tablespaceId,
 					 collationObjectId, classObjectId,
-					 coloptions, reloptions, stmt->primary,
+					 coloptions, reloptions, stmt->primary, stmt->isindirect,
 					 stmt->isconstraint, stmt->deferrable, stmt->initdeferred,
 					 allowSystemTableMods,
 					 skip_build || stmt->concurrent,
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 009c1b7..302fc60 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -273,7 +273,8 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 					  EState *estate,
 					  bool noDupErr,
 					  bool *specConflict,
-					  List *arbiterIndexes)
+					  List *arbiterIndexes,
+					  Bitmapset *unchangedAttrs)
 {
 	List	   *result = NIL;
 	ResultRelInfo *resultRelInfo;
@@ -285,6 +286,7 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 	ExprContext *econtext;
 	Datum		values[INDEX_MAX_KEYS];
 	bool		isnull[INDEX_MAX_KEYS];
+	Datum		pkeyValues[INDEX_MAX_KEYS];
 
 	/*
 	 * Get information from the result relation info structure.
@@ -348,6 +350,27 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 		}
 
 		/*
+		 * For indirect indexes, verify whether the indexed attributes have
+		 * changed; if they have not, skip the insertion.
+		 */
+		if (indexInfo->ii_IsIndirect)
+		{
+			int		j;
+			bool	may_skip_insertion = true;
+
+			for (j = 0; j < indexInfo->ii_NumIndexAttrs; j++)
+			{
+				if (bms_is_member(indexInfo->ii_KeyAttrNumbers[j], unchangedAttrs))
+					continue;
+				may_skip_insertion = false;
+			}
+
+			/* may skip insertion if no indexed attribute changed value */
+			if (may_skip_insertion)
+				continue;
+		}
+
+		/*
 		 * FormIndexDatum fills in its values and isnull parameters with the
 		 * appropriate values for the column(s) of the index.
 		 */
@@ -357,6 +380,11 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 					   values,
 					   isnull);
 
+		/* If this is the primary key, save the values for later */
+		if (i == 0)
+			/* XXX probably need datumCopy here instead */
+			memcpy(pkeyValues, values, sizeof(Datum) * INDEX_MAX_KEYS);
+
 		/* Check whether to apply noDupErr to this index */
 		applyNoDupErr = noDupErr &&
 			(arbiterIndexes == NIL ||
@@ -389,6 +417,7 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 			index_insert(indexRelation, /* index relation */
 						 values,	/* array of index Datums */
 						 isnull,	/* null flags */
+						 pkeyValues,	/* values of primary key */
 						 tupleid,		/* tid of heap tuple */
 						 heapRelation,	/* heap relation */
 						 checkUnique);	/* type of uniqueness check to do */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index efb0c5e..2503711 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -449,7 +449,8 @@ ExecInsert(ModifyTableState *mtstate,
 			/* insert index entries for tuple */
 			recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
 												 estate, true, &specConflict,
-												   arbiterIndexes);
+												   arbiterIndexes,
+												   NULL);
 
 			/* adjust the tuple's state accordingly */
 			if (!specConflict)
@@ -495,7 +496,8 @@ ExecInsert(ModifyTableState *mtstate,
 			if (resultRelInfo->ri_NumIndices > 0)
 				recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
 													   estate, false, NULL,
-													   arbiterIndexes);
+													   arbiterIndexes,
+													   NULL);
 		}
 	}
 
@@ -895,6 +897,7 @@ ExecUpdate(ItemPointer tupleid,
 	else
 	{
 		LockTupleMode lockmode;
+		Bitmapset  *unchangedAttrs = NULL;
 
 		/*
 		 * Constraints might reference the tableoid column, so initialize
@@ -938,7 +941,9 @@ lreplace:;
 							 estate->es_output_cid,
 							 estate->es_crosscheck_snapshot,
 							 true /* wait for commit */ ,
-							 &hufd, &lockmode);
+							 &hufd,
+							 &unchangedAttrs,
+							 &lockmode);
 		switch (result)
 		{
 			case HeapTupleSelfUpdated:
@@ -1028,7 +1033,8 @@ lreplace:;
 		 */
 		if (resultRelInfo->ri_NumIndices > 0 && !HeapTupleIsHeapOnly(tuple))
 			recheckIndexes = ExecInsertIndexTuples(slot, &(tuple->t_self),
-												   estate, false, NULL, NIL);
+												   estate, false, NULL, NIL,
+												   unchangedAttrs);
 	}
 
 	if (canSetTag)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 71714bc..9880321 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3145,6 +3145,7 @@ _copyIndexStmt(const IndexStmt *from)
 	COPY_SCALAR_FIELD(indexOid);
 	COPY_SCALAR_FIELD(oldNode);
 	COPY_SCALAR_FIELD(unique);
+	COPY_SCALAR_FIELD(isindirect);
 	COPY_SCALAR_FIELD(primary);
 	COPY_SCALAR_FIELD(isconstraint);
 	COPY_SCALAR_FIELD(deferrable);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 29a090f..b91d924 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1271,6 +1271,7 @@ _equalIndexStmt(const IndexStmt *a, const IndexStmt *b)
 	COMPARE_SCALAR_FIELD(indexOid);
 	COMPARE_SCALAR_FIELD(oldNode);
 	COMPARE_SCALAR_FIELD(unique);
+	COMPARE_SCALAR_FIELD(isindirect);
 	COMPARE_SCALAR_FIELD(primary);
 	COMPARE_SCALAR_FIELD(isconstraint);
 	COMPARE_SCALAR_FIELD(deferrable);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index ae86954..4f3b864 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2449,6 +2449,7 @@ _outIndexStmt(StringInfo str, const IndexStmt *node)
 	WRITE_OID_FIELD(indexOid);
 	WRITE_OID_FIELD(oldNode);
 	WRITE_BOOL_FIELD(unique);
+	WRITE_BOOL_FIELD(isindirect);
 	WRITE_BOOL_FIELD(primary);
 	WRITE_BOOL_FIELD(isconstraint);
 	WRITE_BOOL_FIELD(deferrable);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5547fc8..cecd6d4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -410,7 +410,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	overlay_placing substr_from substr_for
 
 %type <boolean> opt_instead
-%type <boolean> opt_unique opt_concurrently opt_verbose opt_full
+%type <boolean> opt_unique opt_indirect opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_default opt_recheck
 %type <defelt>	opt_binary opt_oids copy_delimiter
 
@@ -596,7 +596,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
 	IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P
-	INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
+	INCLUDING INCREMENT INDEX INDEXES INDIRECT
+	INHERIT INHERITS INITIALLY INLINE_P
 	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
 	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
 
@@ -6651,25 +6652,26 @@ defacl_privilege_target:
  * willing to make TABLESPACE a fully reserved word.
  *****************************************************************************/
 
-IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
+IndexStmt:	CREATE opt_unique opt_indirect INDEX opt_concurrently opt_index_name
 			ON qualified_name access_method_clause '(' index_params ')'
 			opt_reloptions OptTableSpace where_clause
 				{
 					IndexStmt *n = makeNode(IndexStmt);
 					n->unique = $2;
-					n->concurrent = $4;
-					n->idxname = $5;
-					n->relation = $7;
-					n->accessMethod = $8;
-					n->indexParams = $10;
-					n->options = $12;
-					n->tableSpace = $13;
-					n->whereClause = $14;
+					n->concurrent = $5;
+					n->idxname = $6;
+					n->relation = $8;
+					n->accessMethod = $9;
+					n->indexParams = $11;
+					n->options = $13;
+					n->tableSpace = $14;
+					n->whereClause = $15;
 					n->excludeOpNames = NIL;
 					n->idxcomment = NULL;
 					n->indexOid = InvalidOid;
 					n->oldNode = InvalidOid;
 					n->primary = false;
+					n->isindirect = $3;
 					n->isconstraint = false;
 					n->deferrable = false;
 					n->initdeferred = false;
@@ -6677,25 +6679,26 @@ IndexStmt:	CREATE opt_unique INDEX opt_concurrently opt_index_name
 					n->if_not_exists = false;
 					$$ = (Node *)n;
 				}
-			| CREATE opt_unique INDEX opt_concurrently IF_P NOT EXISTS index_name
+			| CREATE opt_unique opt_indirect INDEX opt_concurrently IF_P NOT EXISTS index_name
 			ON qualified_name access_method_clause '(' index_params ')'
 			opt_reloptions OptTableSpace where_clause
 				{
 					IndexStmt *n = makeNode(IndexStmt);
 					n->unique = $2;
-					n->concurrent = $4;
-					n->idxname = $8;
-					n->relation = $10;
-					n->accessMethod = $11;
-					n->indexParams = $13;
-					n->options = $15;
-					n->tableSpace = $16;
-					n->whereClause = $17;
+					n->concurrent = $5;
+					n->idxname = $9;
+					n->relation = $11;
+					n->accessMethod = $12;
+					n->indexParams = $14;
+					n->options = $16;
+					n->tableSpace = $17;
+					n->whereClause = $18;
 					n->excludeOpNames = NIL;
 					n->idxcomment = NULL;
 					n->indexOid = InvalidOid;
 					n->oldNode = InvalidOid;
 					n->primary = false;
+					n->isindirect = $3;
 					n->isconstraint = false;
 					n->deferrable = false;
 					n->initdeferred = false;
@@ -6710,6 +6713,11 @@ opt_unique:
 			| /*EMPTY*/								{ $$ = FALSE; }
 		;
 
+opt_indirect:
+			INDIRECT								{ $$ = TRUE; }
+			| /*EMPTY*/								{ $$ = FALSE; }
+		;
+
 opt_concurrently:
 			CONCURRENTLY							{ $$ = TRUE; }
 			| /*EMPTY*/								{ $$ = FALSE; }
@@ -13775,6 +13783,7 @@ unreserved_keyword:
 			| INCREMENT
 			| INDEX
 			| INDEXES
+			| INDIRECT
 			| INHERIT
 			| INHERITS
 			| INLINE_P
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 79e0b1f..939ea34 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -266,7 +266,7 @@ static TupleDesc GetPgIndexDescriptor(void);
 static void AttrDefaultFetch(Relation relation);
 static void CheckConstraintFetch(Relation relation);
 static int	CheckConstraintCmp(const void *a, const void *b);
-static List *insert_ordered_oid(List *list, Oid datum);
+static List *insert_ordered_oid(List *list, Oid datum, bool must_be_first);
 static void InitIndexAmRoutine(Relation relation);
 static void IndexSupportInitialize(oidvector *indclass,
 					   RegProcedure *indexSupport,
@@ -2032,6 +2032,7 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc)
 	bms_free(relation->rd_indexattr);
 	bms_free(relation->rd_keyattr);
 	bms_free(relation->rd_idattr);
+	bms_free(relation->rd_indirectattr);
 	if (relation->rd_options)
 		pfree(relation->rd_options);
 	if (relation->rd_indextuple)
@@ -3942,6 +3943,47 @@ RelationGetFKeyList(Relation relation)
 }
 
 /*
+ * Return the relation's primary key OID.
+ *
+ * Surely this can be made better ...
+ */
+Oid
+RelationGetPrimaryKey(Relation relation)
+{
+	Relation	indrel;
+	SysScanDesc indscan;
+	ScanKeyData skey;
+	HeapTuple	htup;
+	Oid			pkid = InvalidOid;
+
+	/* Currently we just scan pg_index every time this is called */
+	ScanKeyInit(&skey,
+				Anum_pg_index_indrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+
+	indrel = heap_open(IndexRelationId, AccessShareLock);
+	indscan = systable_beginscan(indrel, IndexIndrelidIndexId, true,
+								 NULL, 1, &skey);
+	while (HeapTupleIsValid(htup = systable_getnext(indscan)))
+	{
+		Form_pg_index index = (Form_pg_index) GETSTRUCT(htup);
+
+		if (!IndexIsLive(index))
+			continue;
+		if (!index->indisprimary)
+			continue;
+		pkid = index->indexrelid;
+		break;
+	}
+
+	systable_endscan(indscan);
+	heap_close(indrel, AccessShareLock);
+
+	return pkid;
+}
+
+/*
  * RelationGetIndexList -- get a list of OIDs of indexes on this relation
  *
  * The index list is created only if someone requests it.  We scan pg_index
@@ -3955,7 +3997,8 @@ RelationGetFKeyList(Relation relation)
  * Such indexes are expected to be dropped momentarily, and should not be
  * touched at all by any caller of this function.
  *
- * The returned list is guaranteed to be sorted in order by OID.  This is
+ * The returned list is guaranteed to be sorted in order by OID, except that
+ * the primary key is always in front.  This is
  * needed by the executor, since for index types that we obtain exclusive
  * locks on when updating the index, all backends must lock the indexes in
  * the same order or we will get deadlocks (see ExecOpenIndices()).  Any
@@ -4031,7 +4074,8 @@ RelationGetIndexList(Relation relation)
 			continue;
 
 		/* Add index's OID to result list in the proper order */
-		result = insert_ordered_oid(result, index->indexrelid);
+		result = insert_ordered_oid(result, index->indexrelid,
+									index->indisprimary);
 
 		/*
 		 * indclass cannot be referenced directly through the C struct,
@@ -4104,12 +4148,12 @@ RelationGetIndexList(Relation relation)
  * indexes...
  */
 static List *
-insert_ordered_oid(List *list, Oid datum)
+insert_ordered_oid(List *list, Oid datum, bool must_be_first)
 {
 	ListCell   *prev;
 
 	/* Does the datum belong at the front? */
-	if (list == NIL || datum < linitial_oid(list))
+	if (list == NIL || datum < linitial_oid(list) || must_be_first)
 		return lcons_oid(datum, list);
 	/* No, so find the entry it belongs after */
 	prev = list_head(list);
@@ -4146,7 +4190,7 @@ insert_ordered_oid(List *list, Oid datum)
  * to ensure that a correct rd_indexattr set has been cached before first
  * calling RelationSetIndexList; else a subsequent inquiry might cause a
  * wrong rd_indexattr set to get computed and cached.  Likewise, we do not
- * touch rd_keyattr or rd_idattr.
+ * touch rd_keyattr, rd_indirectattr or rd_idattr.
  */
 void
 RelationSetIndexList(Relation relation, List *indexIds, Oid oidIndex)
@@ -4375,6 +4419,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	Bitmapset  *indexattrs;		/* indexed columns */
 	Bitmapset  *uindexattrs;	/* columns in unique indexes */
 	Bitmapset  *idindexattrs;	/* columns in the replica identity */
+	Bitmapset  *indirectattrs;	/* columns in indirect indexes */
 	List	   *indexoidlist;
 	Oid			relreplindex;
 	ListCell   *l;
@@ -4391,6 +4436,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 				return bms_copy(relation->rd_keyattr);
 			case INDEX_ATTR_BITMAP_IDENTITY_KEY:
 				return bms_copy(relation->rd_idattr);
+			case INDEX_ATTR_BITMAP_INDIRECT_INDEXES:
+				return bms_copy(relation->rd_indirectattr);
 			default:
 				elog(ERROR, "unknown attrKind %u", attrKind);
 		}
@@ -4419,7 +4466,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	relreplindex = relation->rd_replidindex;
 
 	/*
-	 * For each index, add referenced attributes to indexattrs.
+	 * For each index, add referenced attributes to the attribute bitmaps.
 	 *
 	 * Note: we consider all indexes returned by RelationGetIndexList, even if
 	 * they are not indisready or indisvalid.  This is important because an
@@ -4431,6 +4478,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	indexattrs = NULL;
 	uindexattrs = NULL;
 	idindexattrs = NULL;
+	indirectattrs = NULL;
 	foreach(l, indexoidlist)
 	{
 		Oid			indexOid = lfirst_oid(l);
@@ -4439,6 +4487,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 		int			i;
 		bool		isKey;		/* candidate key */
 		bool		isIDKey;	/* replica identity index */
+		bool		isIndirect;	/* an indirect index */
+		Bitmapset  *exprattrs = NULL;
 
 		indexDesc = index_open(indexOid, AccessShareLock);
 
@@ -4453,6 +4503,9 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 		/* Is this index the configured (or default) replica identity? */
 		isIDKey = (indexOid == relreplindex);
 
+		/* Is this an indirect index? */
+		isIndirect = indexInfo->ii_IsIndirect;
+
 		/* Collect simple attribute references */
 		for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
 		{
@@ -4460,8 +4513,10 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 
 			if (attrnum != 0)
 			{
-				indexattrs = bms_add_member(indexattrs,
-							   attrnum - FirstLowInvalidHeapAttributeNumber);
+				/* FIXME the name of this is now a lie. Must fix! */
+				if (!isIndirect)
+					indexattrs = bms_add_member(indexattrs,
+												attrnum - FirstLowInvalidHeapAttributeNumber);
 
 				if (isKey)
 					uindexattrs = bms_add_member(uindexattrs,
@@ -4470,11 +4525,18 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 				if (isIDKey)
 					idindexattrs = bms_add_member(idindexattrs,
 							   attrnum - FirstLowInvalidHeapAttributeNumber);
+
+				if (isIndirect)
+					indirectattrs = bms_add_member(indirectattrs,
+							   attrnum - FirstLowInvalidHeapAttributeNumber);
 			}
 		}
 
 		/* Collect all attributes used in expressions, too */
-		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
+		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &exprattrs);
+		indexattrs = bms_add_members(indexattrs, exprattrs);
+		indirectattrs = bms_add_members(indirectattrs, exprattrs);
+		bms_free(exprattrs);
 
 		/* Collect all attributes in the index predicate, too */
 		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
@@ -4491,6 +4553,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	relation->rd_keyattr = NULL;
 	bms_free(relation->rd_idattr);
 	relation->rd_idattr = NULL;
+	bms_free(relation->rd_indirectattr);
+	relation->rd_indirectattr = NULL;
 
 	/*
 	 * Now save copies of the bitmaps in the relcache entry.  We intentionally
@@ -4503,6 +4567,7 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	relation->rd_keyattr = bms_copy(uindexattrs);
 	relation->rd_idattr = bms_copy(idindexattrs);
 	relation->rd_indexattr = bms_copy(indexattrs);
+	relation->rd_indirectattr = bms_copy(indirectattrs);
 	MemoryContextSwitchTo(oldcxt);
 
 	/* We return our original working copy for caller to play with */
@@ -4514,6 +4579,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 			return uindexattrs;
 		case INDEX_ATTR_BITMAP_IDENTITY_KEY:
 			return idindexattrs;
+		case INDEX_ATTR_BITMAP_INDIRECT_INDEXES:
+			return indirectattrs;
 		default:
 			elog(ERROR, "unknown attrKind %u", attrKind);
 			return NULL;
@@ -5059,6 +5126,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_indexattr = NULL;
 		rel->rd_keyattr = NULL;
 		rel->rd_idattr = NULL;
+		rel->rd_indirectattr = NULL;
 		rel->rd_createSubid = InvalidSubTransactionId;
 		rel->rd_newRelfilenodeSubid = InvalidSubTransactionId;
 		rel->rd_amcache = NULL;
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 1036cca..9f28c7d 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -175,6 +175,8 @@ typedef struct IndexAmRoutine
 	bool		amclusterable;
 	/* does AM handle predicate locks? */
 	bool		ampredlocks;
+	/* does AM support indirect indexes? */
+	bool		amcanindirect;
 	/* type of data stored in index, or InvalidOid if variable */
 	Oid			amkeytype;
 
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 81907d5..c247b89 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -102,13 +102,17 @@ typedef struct SysScanDescData *SysScanDesc;
  * call is made with UNIQUE_CHECK_EXISTING.  The tuple is already in the
  * index in this case, so it should not be inserted again.  Rather, just
  * check for conflicting live tuples (possibly blocking).
+ *
+ * UNIQUE_CHECK_INSERT_SINGLETON only inserts if there isn't already an
+ * index tuple.   This is supported for indirect indexes.
  */
 typedef enum IndexUniqueCheck
 {
 	UNIQUE_CHECK_NO,			/* Don't do any uniqueness checking */
 	UNIQUE_CHECK_YES,			/* Enforce uniqueness at insertion time */
 	UNIQUE_CHECK_PARTIAL,		/* Test uniqueness, but no error */
-	UNIQUE_CHECK_EXISTING		/* Check if existing tuple is unique */
+	UNIQUE_CHECK_EXISTING,		/* Check if existing tuple is unique */
+	UNIQUE_CHECK_INSERT_SINGLETON	/* Only insert if value doesn't exist */
 } IndexUniqueCheck;
 
 
@@ -127,6 +131,7 @@ extern void index_close(Relation relation, LOCKMODE lockmode);
 
 extern bool index_insert(Relation indexRelation,
 			 Datum *values, bool *isnull,
+			 Datum *pkeyValues,
 			 ItemPointer heap_t_ctid,
 			 Relation heapRelation,
 			 IndexUniqueCheck checkUnique);
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 0d12bbb..99af368 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -16,6 +16,7 @@
 
 #include "access/sdir.h"
 #include "access/skey.h"
+#include "nodes/bitmapset.h"
 #include "nodes/lockoptions.h"
 #include "nodes/primnodes.h"
 #include "storage/bufpage.h"
@@ -160,7 +161,8 @@ extern void heap_abort_speculative(Relation relation, HeapTuple tuple);
 extern HTSU_Result heap_update(Relation relation, ItemPointer otid,
 			HeapTuple newtup,
 			CommandId cid, Snapshot crosscheck, bool wait,
-			HeapUpdateFailureData *hufd, LockTupleMode *lockmode);
+			HeapUpdateFailureData *hufd, Bitmapset **unchanged_attrs,
+			LockTupleMode *lockmode);
 extern HTSU_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
 				CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy,
 				bool follow_update,
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 37e6ef3..2f21db2 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -37,6 +37,17 @@ typedef enum
 	INDEX_DROP_SET_DEAD
 } IndexStateFlagsAction;
 
+static inline void
+FAKE_CTID_FROM_PKVALUES(ItemPointer iptr, int16 pkNumKeys, Datum *pkvalues)
+{
+	/* XXX should eventually support more than one column */
+	Assert(pkNumKeys == 1);
+
+	ItemPointerSet(iptr,
+				   GET_4_BYTES(pkvalues[0]),
+				   GET_2_BYTES(pkvalues[0] << 32));
+	Assert(GET_2_BYTES(pkvalues[0] << 48) == 0);
+}
 
 extern void index_check_primary_key(Relation heapRel,
 						IndexInfo *indexInfo,
@@ -55,6 +66,7 @@ extern Oid index_create(Relation heapRelation,
 			 int16 *coloptions,
 			 Datum reloptions,
 			 bool isprimary,
+			 bool isindirect,
 			 bool isconstraint,
 			 bool deferrable,
 			 bool initdeferred,
diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h
index ee97c5d..144d204 100644
--- a/src/include/catalog/pg_index.h
+++ b/src/include/catalog/pg_index.h
@@ -34,6 +34,7 @@ CATALOG(pg_index,2610) BKI_WITHOUT_OIDS BKI_SCHEMA_MACRO
 	Oid			indrelid;		/* OID of the relation it indexes */
 	int16		indnatts;		/* number of columns in index */
 	bool		indisunique;	/* is this a unique index? */
+	bool		indisindirect;	/* is this an indirect index? */
 	bool		indisprimary;	/* is this index for primary key? */
 	bool		indisexclusion; /* is this index for exclusion constraint? */
 	bool		indimmediate;	/* is uniqueness enforced immediately? */
@@ -70,26 +71,27 @@ typedef FormData_pg_index *Form_pg_index;
  *		compiler constants for pg_index
  * ----------------
  */
-#define Natts_pg_index					19
+#define Natts_pg_index					20
 #define Anum_pg_index_indexrelid		1
 #define Anum_pg_index_indrelid			2
 #define Anum_pg_index_indnatts			3
 #define Anum_pg_index_indisunique		4
-#define Anum_pg_index_indisprimary		5
-#define Anum_pg_index_indisexclusion	6
-#define Anum_pg_index_indimmediate		7
-#define Anum_pg_index_indisclustered	8
-#define Anum_pg_index_indisvalid		9
-#define Anum_pg_index_indcheckxmin		10
-#define Anum_pg_index_indisready		11
-#define Anum_pg_index_indislive			12
-#define Anum_pg_index_indisreplident	13
-#define Anum_pg_index_indkey			14
-#define Anum_pg_index_indcollation		15
-#define Anum_pg_index_indclass			16
-#define Anum_pg_index_indoption			17
-#define Anum_pg_index_indexprs			18
-#define Anum_pg_index_indpred			19
+#define Anum_pg_index_indisindirect		5
+#define Anum_pg_index_indisprimary		6
+#define Anum_pg_index_indisexclusion	7
+#define Anum_pg_index_indimmediate		8
+#define Anum_pg_index_indisclustered	9
+#define Anum_pg_index_indisvalid		10
+#define Anum_pg_index_indcheckxmin		11
+#define Anum_pg_index_indisready		12
+#define Anum_pg_index_indislive			13
+#define Anum_pg_index_indisreplident	14
+#define Anum_pg_index_indkey			15
+#define Anum_pg_index_indcollation		16
+#define Anum_pg_index_indclass			17
+#define Anum_pg_index_indoption			18
+#define Anum_pg_index_indexprs			19
+#define Anum_pg_index_indpred			20
 
 /*
  * Index AMs that support ordered scans must support these two indoption
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 136276b..39230c1 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -367,7 +367,7 @@ extern void ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative);
 extern void ExecCloseIndices(ResultRelInfo *resultRelInfo);
 extern List *ExecInsertIndexTuples(TupleTableSlot *slot, ItemPointer tupleid,
 					  EState *estate, bool noDupErr, bool *specConflict,
-					  List *arbiterIndexes);
+					  List *arbiterIndexes, Bitmapset *unchangedAttrs);
 extern bool ExecCheckIndexConstraints(TupleTableSlot *slot, EState *estate,
 						  ItemPointer conflictTid, List *arbiterIndexes);
 extern void check_exclusion_constraint(Relation heap, Relation index,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f6f73f3..f6c4655 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -48,6 +48,7 @@
  *		UniqueProcs
  *		UniqueStrats
  *		Unique				is it a unique index?
+ *		IsIndirect			is it an indirect index?
  *		ReadyForInserts		is it valid for inserts?
  *		Concurrent			are we doing a concurrent index build?
  *		BrokenHotChain		did we detect any broken HOT chains?
@@ -72,6 +73,7 @@ typedef struct IndexInfo
 	Oid		   *ii_UniqueProcs; /* array with one entry per column */
 	uint16	   *ii_UniqueStrats;	/* array with one entry per column */
 	bool		ii_Unique;
+	bool		ii_IsIndirect;
 	bool		ii_ReadyForInserts;
 	bool		ii_Concurrent;
 	bool		ii_BrokenHotChain;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 6de2cab..e4ee0af 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2449,6 +2449,7 @@ typedef struct IndexStmt
 	Oid			indexOid;		/* OID of an existing index, if any */
 	Oid			oldNode;		/* relfilenode of existing storage, if any */
 	bool		unique;			/* is index unique? */
+	bool		isindirect;		/* is index indirect? */
 	bool		primary;		/* is index a primary key? */
 	bool		isconstraint;	/* is it for a pkey/unique constraint? */
 	bool		deferrable;		/* is the constraint DEFERRABLE? */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 17ffef5..04b7221 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -194,6 +194,7 @@ PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD)
 PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD)
 PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD)
 PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD)
+PG_KEYWORD("indirect", INDIRECT, UNRESERVED_KEYWORD)
 PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD)
 PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD)
 PG_KEYWORD("initially", INITIALLY, RESERVED_KEYWORD)
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index ed14442..71c4af7 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -103,6 +103,7 @@ typedef struct RelationData
 	Bitmapset  *rd_indexattr;	/* identifies columns used in indexes */
 	Bitmapset  *rd_keyattr;		/* cols that can be ref'd by foreign keys */
 	Bitmapset  *rd_idattr;		/* included in replica identity index */
+	Bitmapset  *rd_indirectattr; /* cols part of any indirect index */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index 6ea7dd2..a0dbb23 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -39,6 +39,7 @@ extern void RelationClose(Relation relation);
  */
 extern List *RelationGetFKeyList(Relation relation);
 extern List *RelationGetIndexList(Relation relation);
+extern Oid	RelationGetPrimaryKey(Relation relation);
 extern Oid	RelationGetOidIndex(Relation relation);
 extern Oid	RelationGetReplicaIndex(Relation relation);
 extern List *RelationGetIndexExpressions(Relation relation);
@@ -48,7 +49,8 @@ typedef enum IndexAttrBitmapKind
 {
 	INDEX_ATTR_BITMAP_ALL,
 	INDEX_ATTR_BITMAP_KEY,
-	INDEX_ATTR_BITMAP_IDENTITY_KEY
+	INDEX_ATTR_BITMAP_IDENTITY_KEY,
+	INDEX_ATTR_BITMAP_INDIRECT_INDEXES
 } IndexAttrBitmapKind;
 
 extern Bitmapset *RelationGetIndexAttrBitmap(Relation relation,

view thread (61+ messages)  latest in thread

Message-ID: <20161101044331.4w73ywvi42niltva@alvherre.pgsql>
Permalink:  ../20161101044331.4w73ywvi42niltva@alvherre.pgsql/
Also on:    postgresql.org/message-id/20161101044331.4w73ywvi42niltva@alvherre.pgsql

 ·  · 

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: pgsql-hackers@postgresql.org
  Cc: alvherre@2ndquadrant.com
  Subject: Re: Indirect indexes
  In-Reply-To: <20161101044331.4w73ywvi42niltva@alvherre.pgsql>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox