index-only-scans-v1.patch

application/octet-stream

Filename: index-only-scans-v1.patch
Type: application/octet-stream
Part: 0
Message: index-only scans
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index fe3aa3c..28ad480 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -114,6 +114,8 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys)
 	scan->xs_ctup.t_data = NULL;
 	scan->xs_cbuf = InvalidBuffer;
 	scan->xs_continue_hot = false;
+	scan->want_index_tuple = false;
+	scan->xs_itup = NULL;
 
 	return scan;
 }
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 13e68d6..0f544f4 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -20,6 +20,8 @@
  *		index_insert	- insert an index tuple into a relation
  *		index_markpos	- mark a scan position
  *		index_restrpos	- restore a scan position
+ *		index_getnexttid	- get the next TID from a scan
+ *		index_fetch		- get the tuple of specified TID
  *		index_getnext	- get the next tuple from a scan
  *		index_getbitmap - get all tuples from a scan
  *		index_bulk_delete	- bulk deletion of index tuples
@@ -425,6 +427,133 @@ index_restrpos(IndexScanDesc scan)
 }
 
 /* ----------------
+ * index_getnexttid - get the next TID from a scan
+ *
+ * The result is the next TID satisfying the scan keys,
+ * or NULL if no more matching tuples exist.
+ * ----------------
+ */
+ItemPointer
+index_getnexttid(IndexScanDesc scan, ScanDirection direction)
+{
+	FmgrInfo   	*procedure;
+	bool		found;
+
+	SCAN_CHECKS;
+	GET_SCAN_PROCEDURE(amgettuple);
+
+	Assert(TransactionIdIsValid(RecentGlobalXmin));
+
+	/* New index tuple, so not still following a HOT chain... */
+	scan->xs_continue_hot = false;
+
+	/*
+	 * The AM's gettuple proc finds the next index entry matching the
+	 * scan keys, and puts the TID in xs_ctup.t_self (ie, *tid). It
+	 * should also set scan->xs_recheck, though we pay no attention to
+	 * that here.
+	 */
+	found = DatumGetBool(FunctionCall2(procedure,
+					   PointerGetDatum(scan),
+					   Int32GetDatum(direction)));
+
+	/* Reset kill flag immediately for safety */
+	scan->kill_prior_tuple = false;
+
+	/* If we're out of index entries, release any held pin on a heap page */
+	if (!found)
+	{
+		if (BufferIsValid(scan->xs_cbuf))
+		{
+			ReleaseBuffer(scan->xs_cbuf);
+			scan->xs_cbuf = InvalidBuffer;
+		}
+		return NULL;
+	}
+
+	/* Return the TID of the tuple we found. */
+	pgstat_count_index_tuples(scan->indexRelation, 1);
+	return &scan->xs_ctup.t_self;
+}
+
+/* ----------------
+ *		index_fetch - get the heap tuple of specified TID from a scan
+ *
+ * The result is the heap tuple of particular TID, or NULL if no more
+ * matching tuples exist. On success, the buffer containing the heap tuple
+ * is pinned (the pin will be dropped at the next index_getnext,
+ * index_getnexttid or index_endscan).
+ *
+ * Note: caller must check scan->xs_recheck, and perform rechecking of the
+ * scan keys if required.  We do not do that here because we don't have
+ * enough information to do it efficiently in the general case.
+ * There should be at most one call of this function after index_getnextid
+ * function call, and snapshot must be MVCC snapshot.
+ * ----------------
+ */
+HeapTuple
+index_fetch(IndexScanDesc scan)
+{
+	bool		all_dead = false;
+	bool		got_heap_tuple;
+	Buffer		prev_buf;
+	ItemPointer	tid = &scan->xs_ctup.t_self;
+
+	/* We can skip the buffer-switching logic if we're in mid-HOT chain. */
+	if (!scan->xs_continue_hot)
+	{
+
+		/* Switch to correct buffer if we don't have it already */
+		prev_buf = scan->xs_cbuf;
+		scan->xs_cbuf = ReleaseAndReadBuffer(scan->xs_cbuf,
+											 scan->heapRelation,
+											 ItemPointerGetBlockNumber(tid));
+
+		/*
+		 * Prune page, but only if we weren't already on this page
+		 */
+		if (prev_buf != scan->xs_cbuf)
+			heap_page_prune_opt(scan->heapRelation, scan->xs_cbuf,
+								RecentGlobalXmin);
+	}
+
+	/* Obtain share-lock on the buffer so we can examine visibility */
+	LockBuffer(scan->xs_cbuf, BUFFER_LOCK_SHARE);
+	got_heap_tuple = heap_hot_search_buffer(tid, scan->heapRelation,
+											scan->xs_cbuf,
+											scan->xs_snapshot,
+											&scan->xs_ctup,
+											&all_dead,
+											!scan->xs_continue_hot);
+	LockBuffer(scan->xs_cbuf, BUFFER_LOCK_UNLOCK);
+
+	if (got_heap_tuple)
+	{
+		/*
+		 * Only in a non-MVCC snapshot can more than one member of the
+		 * HOT chain be visible.
+		 */
+		scan->xs_continue_hot = !IsMVCCSnapshot(scan->xs_snapshot);
+		pgstat_count_heap_fetch(scan->indexRelation);
+		return &scan->xs_ctup;
+	}
+
+	/* We've reached the end of the HOT chain. */
+	scan->xs_continue_hot = false;
+
+	/*
+	 * If we scanned a whole HOT chain and found only dead tuples, tell
+	 * index AM to kill its entry for that TID. We do not do this when in
+	 * recovery because it may violate MVCC to do so. see comments in
+	 * RelationGetIndexScan().
+	 */
+	if (!scan->xactStartedInRecovery)
+		scan->kill_prior_tuple = all_dead;
+
+	return NULL;
+}
+
+/* ----------------
  *		index_getnext - get the next heap tuple from a scan
  *
  * The result is the next heap tuple satisfying the scan keys and the
@@ -440,20 +569,11 @@ index_restrpos(IndexScanDesc scan)
 HeapTuple
 index_getnext(IndexScanDesc scan, ScanDirection direction)
 {
-	HeapTuple	heapTuple = &scan->xs_ctup;
-	ItemPointer tid = &heapTuple->t_self;
-	FmgrInfo   *procedure;
-	bool		all_dead = false;
-
-	SCAN_CHECKS;
-	GET_SCAN_PROCEDURE(amgettuple);
-
-	Assert(TransactionIdIsValid(RecentGlobalXmin));
+	HeapTuple	heapTuple;
+	ItemPointer tid;
 
 	for (;;)
 	{
-		bool	got_heap_tuple;
-
 		if (scan->xs_continue_hot)
 		{
 			/*
@@ -461,86 +581,26 @@ index_getnext(IndexScanDesc scan, ScanDirection direction)
 			 * earlier member.	Must still hold pin on current heap page.
 			 */
 			Assert(BufferIsValid(scan->xs_cbuf));
-			Assert(ItemPointerGetBlockNumber(tid) ==
+			Assert(ItemPointerGetBlockNumber(&scan->xs_ctup.t_self) ==
 				   BufferGetBlockNumber(scan->xs_cbuf));
 		}
 		else
 		{
-			bool		found;
-			Buffer		prev_buf;
-
-			/*
-			 * If we scanned a whole HOT chain and found only dead tuples,
-			 * tell index AM to kill its entry for that TID. We do not do this
-			 * when in recovery because it may violate MVCC to do so. see
-			 * comments in RelationGetIndexScan().
-			 */
-			if (!scan->xactStartedInRecovery)
-				scan->kill_prior_tuple = all_dead;
-
-			/*
-			 * The AM's gettuple proc finds the next index entry matching the
-			 * scan keys, and puts the TID in xs_ctup.t_self (ie, *tid). It
-			 * should also set scan->xs_recheck, though we pay no attention to
-			 * that here.
-			 */
-			found = DatumGetBool(FunctionCall2(procedure,
-											   PointerGetDatum(scan),
-											   Int32GetDatum(direction)));
-
-			/* Reset kill flag immediately for safety */
-			scan->kill_prior_tuple = false;
+			tid = index_getnexttid(scan, direction);
 
 			/* If we're out of index entries, break out of outer loop */
-			if (!found)
+			if (!tid)
 				break;
-
-			pgstat_count_index_tuples(scan->indexRelation, 1);
-
-			/* Switch to correct buffer if we don't have it already */
-			prev_buf = scan->xs_cbuf;
-			scan->xs_cbuf = ReleaseAndReadBuffer(scan->xs_cbuf,
-												 scan->heapRelation,
-											 ItemPointerGetBlockNumber(tid));
-
-			/*
-			 * Prune page, but only if we weren't already on this page
-			 */
-			if (prev_buf != scan->xs_cbuf)
-				heap_page_prune_opt(scan->heapRelation, scan->xs_cbuf,
-									RecentGlobalXmin);
 		}
 
-		/* Obtain share-lock on the buffer so we can examine visibility */
-		LockBuffer(scan->xs_cbuf, BUFFER_LOCK_SHARE);
-		got_heap_tuple = heap_hot_search_buffer(tid, scan->heapRelation,
-												scan->xs_cbuf,
-												scan->xs_snapshot,
-												&scan->xs_ctup,
-												&all_dead,
-												!scan->xs_continue_hot);
-		LockBuffer(scan->xs_cbuf, BUFFER_LOCK_UNLOCK);
-
-		if (got_heap_tuple)
-		{
-			/*
-			 * Only in a non-MVCC snapshot can more than one member of the
-			 * HOT chain be visible.
-			 */
-			scan->xs_continue_hot = !IsMVCCSnapshot(scan->xs_snapshot);
-			pgstat_count_heap_fetch(scan->indexRelation);
+		/*
+		 * Fetch the next (or only) heap tuple for this index entry.  If
+		 * we don't find anything, loop around and grab the next tid from
+		 * the index.
+		 */
+		heapTuple = index_fetch(scan);
+		if (heapTuple != NULL)
 			return heapTuple;
-		}
-
-		/* Loop around to ask index AM for another TID */
-		scan->xs_continue_hot = false;
-	}
-
-	/* Release any held pin on a heap page */
-	if (BufferIsValid(scan->xs_cbuf))
-	{
-		ReleaseBuffer(scan->xs_cbuf);
-		scan->xs_cbuf = InvalidBuffer;
 	}
 
 	return NULL;				/* failure exit */
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index ac86eb4..7d39948 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -77,7 +77,7 @@ static void btvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 			 BTCycleId cycleid);
 static void btvacuumpage(BTVacState *vstate, BlockNumber blkno,
 			 BlockNumber orig_blkno);
-
+static IndexTuple bt_getindextuple(IndexScanDesc scan);
 
 /*
  *	btbuild() -- build a new btree index.
@@ -314,9 +314,83 @@ btgettuple(PG_FUNCTION_ARGS)
 	else
 		res = _bt_first(scan, dir);
 
+	if (scan->want_index_tuple)
+	{
+		if (scan->xs_itup != NULL)
+		{
+			pfree(scan->xs_itup);
+			scan->xs_itup = NULL;
+		}
+		if (res)
+			scan->xs_itup = bt_getindextuple(scan);
+	}
+
 	PG_RETURN_BOOL(res);
 }
 
+
+/*
+ * bt_getindextuple - fetch index tuple at current position.
+ *
+ * The caller must have pin on so->currPos.buf.
+ *
+ * The tuple returned is a palloc'd copy. This can fail to find the tuple if
+ * new tuples have been inserted to the page since we stepped on this index
+ * page. NULL is returned in that case.
+ */
+static IndexTuple
+bt_getindextuple(IndexScanDesc scan)
+{
+	BTScanOpaque so = (BTScanOpaque) scan->opaque;
+	Page		page;
+	BTPageOpaque opaque;
+	OffsetNumber minoff;
+	OffsetNumber maxoff;
+	IndexTuple	ituple, result;
+	int itemIndex;
+	BTScanPosItem *positem;
+	OffsetNumber offnum;
+
+	Assert(BufferIsValid(so->currPos.buf));
+
+	LockBuffer(so->currPos.buf, BT_READ);
+
+	page = BufferGetPage(so->currPos.buf);
+	opaque = (BTPageOpaque) PageGetSpecialPointer(page);
+	minoff = P_FIRSTDATAKEY(opaque);
+	maxoff = PageGetMaxOffsetNumber(page);
+
+	itemIndex = so->currPos.itemIndex;
+	positem = &so->currPos.items[itemIndex];
+	offnum = positem->indexOffset;
+
+	Assert(itemIndex >= so->currPos.firstItem &&
+		   itemIndex <= so->currPos.lastItem);
+	if (offnum < minoff)
+	{
+		/* pure paranoia */
+		LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
+		return NULL;
+	}
+
+	ituple = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum));
+
+	if (ItemPointerEquals(&ituple->t_tid, &scan->xs_ctup.t_self))
+	{
+		/* found the item */
+		Size itupsz = IndexTupleSize(ituple);
+
+		result = palloc(itupsz);
+		memcpy(result, ituple, itupsz);
+	}
+	else
+		result = NULL;
+
+	LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
+
+	return result;
+}
+
 /*
  * btgetbitmap() -- gets all matching tuples, and adds them to a bitmap
  */
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index fee829f..bcfbc70 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -661,7 +661,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			pname = sname = "Seq Scan";
 			break;
 		case T_IndexScan:
-			pname = sname = "Index Scan";
+			sname = "Index Scan";
+			if (((IndexScan *) plan)->try_index_only_scan)
+				pname = "Index Only Scan";
+			else
+				pname = "Index Scan";
 			break;
 		case T_BitmapIndexScan:
 			pname = sname = "Bitmap Index Scan";
@@ -828,6 +832,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					}
 					ExplainPropertyText("Scan Direction", scandir, es);
 					ExplainPropertyText("Index Name", indexname, es);
+					ExplainPropertyText("Index Only Scan",
+							    indexscan->try_index_only_scan? "True" : "False",
+							    es);
 				}
 			}
 			/* FALL THRU */
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 263f3b9..b9e23b7 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -27,6 +27,8 @@
 #include "access/genam.h"
 #include "access/nbtree.h"
 #include "access/relscan.h"
+#include "access/sysattr.h"
+#include "access/visibilitymap.h"
 #include "executor/execdebug.h"
 #include "executor/nodeIndexscan.h"
 #include "optimizer/clauses.h"
@@ -37,6 +39,8 @@
 
 
 static TupleTableSlot *IndexNext(IndexScanState *node);
+static void IndexStoreHeapTuple(TupleTableSlot *slot, IndexScan *plan,
+					IndexScanDesc scandesc);
 
 
 /* ----------------------------------------------------------------
@@ -52,17 +56,20 @@ IndexNext(IndexScanState *node)
 	EState	   *estate;
 	ExprContext *econtext;
 	ScanDirection direction;
+	IndexScan  *plan;
 	IndexScanDesc scandesc;
 	HeapTuple	tuple;
 	TupleTableSlot *slot;
+	ItemPointer tid;
 
 	/*
 	 * extract necessary information from index scan node
 	 */
+	plan = (IndexScan *) node->ss.ps.plan;
 	estate = node->ss.ps.state;
 	direction = estate->es_direction;
 	/* flip direction if this is an overall backward scan */
-	if (ScanDirectionIsBackward(((IndexScan *) node->ss.ps.plan)->indexorderdir))
+	if (ScanDirectionIsBackward(plan->indexorderdir))
 	{
 		if (ScanDirectionIsForward(direction))
 			direction = BackwardScanDirection;
@@ -74,18 +81,46 @@ IndexNext(IndexScanState *node)
 	slot = node->ss.ss_ScanTupleSlot;
 
 	/*
-	 * ok, now that we have what we need, fetch the next tuple.
+	 * ok, now that we have what we need, fetch the next tid.
 	 */
-	while ((tuple = index_getnext(scandesc, direction)) != NULL)
+	while ((tid = index_getnexttid(scandesc, direction)) != NULL)
 	{
+		/* Attempt index-only scan, if possible. */
+		if (scandesc->want_index_tuple && scandesc->xs_itup != NULL &&
+			visibilitymap_test(scandesc->heapRelation,
+							   ItemPointerGetBlockNumber(tid),
+							   &node->iss_VMBuffer))
+		{
+			/* Lossy scan shouldn't return tuple. */
+			Assert(!scandesc->xs_recheck);
+
+			/* Store tuple in slot. */
+			IndexStoreHeapTuple(slot, plan, scandesc);
+			return slot;
+		}
+
+		/* Index-only approach failed for some reason, so fetch heap tuple. */
+		tuple = index_fetch(scandesc);
+		if (tuple == NULL)
+			continue;
+
 		/*
-		 * Store the scanned tuple in the scan tuple slot of the scan state.
+		 * Only MVCC snapshots are supported here, so there should be no
+		 * need to keep following the HOT chain once a visible entry has
+		 * been found.
+		 */
+		Assert(!scandesc->xs_continue_hot);
+
+		/*
+		 * Store the scanned tuple in the scan tuple slot of the scan
+		 * state.
+		 *
 		 * Note: we pass 'false' because tuples returned by amgetnext are
 		 * pointers onto disk pages and must not be pfree()'d.
 		 */
 		ExecStoreTuple(tuple,	/* tuple to store */
 					   slot,	/* slot to store in */
-					   scandesc->xs_cbuf,		/* buffer containing tuple */
+					   scandesc->xs_cbuf,	/* buffer containing tuple */
 					   false);	/* don't pfree */
 
 		/*
@@ -389,6 +424,13 @@ ExecEndIndexScan(IndexScanState *node)
 	IndexScanDesc indexScanDesc;
 	Relation	relation;
 
+	/* Release VM buffer pin, if any. */
+	if (node->iss_VMBuffer != InvalidBuffer)
+	{
+		ReleaseBuffer(node->iss_VMBuffer);
+		node->iss_VMBuffer = InvalidBuffer;
+	}
+
 	/*
 	 * extract information from the node
 	 */
@@ -469,6 +511,7 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
 	indexstate = makeNode(IndexScanState);
 	indexstate->ss.ps.plan = (Plan *) node;
 	indexstate->ss.ps.state = estate;
+	indexstate->iss_VMBuffer = InvalidBuffer;
 
 	/*
 	 * Miscellaneous initialization
@@ -617,6 +660,7 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
 					 indexstate->iss_ScanKeys, indexstate->iss_NumScanKeys,
 				indexstate->iss_OrderByKeys, indexstate->iss_NumOrderByKeys);
 
+	indexstate->iss_ScanDesc->want_index_tuple = node->try_index_only_scan;
 	/*
 	 * all done.
 	 */
@@ -1125,3 +1169,71 @@ ExecIndexBuildScanKeys(PlanState *planstate, Relation index, Index scanrelid,
 	else if (n_array_keys != 0)
 		elog(ERROR, "ScalarArrayOpExpr index qual found where not allowed");
 }
+
+/*
+ * IndexStoreHeapTuple
+ *		When performing an index-only scan, we build a faux heap tuple
+ *		from the index tuple.  The missing columns are set to NULL, which
+ *		is OK because we know they're never referenced anyway.
+ */
+static void
+IndexStoreHeapTuple(TupleTableSlot *slot, IndexScan *plan,
+					IndexScanDesc scandesc)
+{
+	int			natts;
+	Datum	   *values;
+	bool	   *isnull;
+	HeapTuple	tuple;
+	int			i;
+
+	natts = scandesc->heapRelation->rd_att->natts;
+	Assert(natts + 1 == plan->nheapatt);
+	values = palloc(sizeof(Datum) * natts);
+	isnull = palloc(sizeof(bool) * natts);
+
+	/* Transpose index tuple into heap tuple. */
+	for (i = 0; i < natts; ++i)
+	{
+		int		indexatt = plan->heapatt[i + 1];
+
+		if (indexatt == 0)
+			isnull[i] = true;
+		else
+		{
+			values[i] = index_getattr(scandesc->xs_itup, indexatt,
+									  scandesc->indexRelation->rd_att,
+									  &isnull[i]);
+		}
+	}
+
+	tuple = heap_form_tuple(scandesc->heapRelation->rd_att, values, isnull);
+
+	/* Done with isnull and values arrays. */
+	pfree(values);
+	pfree(isnull);
+
+	/* If an OID is required, fill it in. */
+	if (scandesc->heapRelation->rd_att->tdhasoid)
+	{
+		if (plan->heapatt[0] == 0)
+			HeapTupleSetOid(tuple, InvalidOid);
+		else
+		{
+			/* We use plan->heapatt[0] to indicate that an OID is needed. */
+			Datum	oid_datum;
+			bool	oid_isnull;
+
+			oid_datum = index_getattr(scandesc->xs_itup, plan->heapatt[0],
+									  scandesc->indexRelation->rd_att,
+									  &oid_isnull);
+			Assert(!oid_isnull);
+			HeapTupleSetOid(tuple, DatumGetObjectId(oid_datum));
+		}
+	}
+
+	/* Store tuple. */
+	ExecStoreTuple(tuple,	/* tuple to store */
+				   slot,	/* slot to store in */
+				   InvalidBuffer,	/* buffer containing tuple */
+				   true);	/* must pfree */
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index d0704ed..2663f52 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -372,6 +372,9 @@ _copyIndexScan(IndexScan *from)
 	COPY_NODE_FIELD(indexorderby);
 	COPY_NODE_FIELD(indexorderbyorig);
 	COPY_SCALAR_FIELD(indexorderdir);
+	COPY_SCALAR_FIELD(try_index_only_scan);
+	COPY_SCALAR_FIELD(nheapatt);
+	COPY_POINTER_FIELD(heapatt, from->nheapatt * sizeof(int));
 
 	return newnode;
 }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 417aeb8..0d2976c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -440,6 +440,8 @@ _outSeqScan(StringInfo str, SeqScan *node)
 static void
 _outIndexScan(StringInfo str, IndexScan *node)
 {
+	int		i;
+
 	WRITE_NODE_TYPE("INDEXSCAN");
 
 	_outScanInfo(str, (Scan *) node);
@@ -450,6 +452,12 @@ _outIndexScan(StringInfo str, IndexScan *node)
 	WRITE_NODE_FIELD(indexorderby);
 	WRITE_NODE_FIELD(indexorderbyorig);
 	WRITE_ENUM_FIELD(indexorderdir, ScanDirection);
+	WRITE_BOOL_FIELD(try_index_only_scan);
+	WRITE_INT_FIELD(nheapatt);
+
+	appendStringInfo(str, " :heapatt");
+	for (i = 0; i < node->nheapatt; i++)
+		appendStringInfo(str, " %d", node->heapatt[i]);
 }
 
 static void
@@ -1510,6 +1518,7 @@ _outIndexPath(StringInfo str, IndexPath *node)
 	WRITE_FLOAT_FIELD(indextotalcost, "%.2f");
 	WRITE_FLOAT_FIELD(indexselectivity, "%.4f");
 	WRITE_FLOAT_FIELD(rows, "%.0f");
+	WRITE_BOOL_FIELD(try_index_only_scan);
 }
 
 static void
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index a3a82ec..7189a99 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -20,6 +20,7 @@
 #include <math.h>
 
 #include "access/skey.h"
+#include "access/sysattr.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -1189,6 +1190,27 @@ create_indexscan_plan(PlannerInfo *root,
 	/* use the indexscan-specific rows estimate, not the parent rel's */
 	scan_plan->scan.plan.plan_rows = best_path->rows;
 
+	/* additional details for index only scans */
+	if (best_path->try_index_only_scan)
+	{
+		int		i;
+
+		scan_plan->try_index_only_scan = true;
+		scan_plan->nheapatt = best_path->path.parent->max_attr + 1;
+		scan_plan->heapatt = palloc0(sizeof(int) * scan_plan->nheapatt);
+		for (i = 0; i < best_path->indexinfo->ncolumns; ++i)
+		{
+			int		indexatt = best_path->indexinfo->indexkeys[i];
+			if (indexatt == ObjectIdAttributeNumber)
+				scan_plan->heapatt[0] = i + 1;
+			else
+			{
+				Assert(indexatt > 0 && indexatt < scan_plan->nheapatt);
+				scan_plan->heapatt[indexatt] = i + 1;
+			}
+		}
+	}
+
 	return scan_plan;
 }
 
@@ -2860,6 +2882,7 @@ make_indexscan(List *qptlist,
 	node->indexorderby = indexorderby;
 	node->indexorderbyorig = indexorderbyorig;
 	node->indexorderdir = indexscandir;
+	node->try_index_only_scan = false;
 
 	return node;
 }
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 161d5ab..f9447e0 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -19,6 +19,7 @@
 #include "catalog/pg_operator.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
+#include "access/sysattr.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
@@ -473,6 +474,50 @@ create_index_path(PlannerInfo *root,
 	pathnode->isjoininner = (outer_rel != NULL);
 	pathnode->indexscandir = indexscandir;
 
+	/*
+	 * If the AM is capable of returning index tuples, check
+	 * whether an index-only scan is possible. We attempt that only when all
+	 * the needed attributes and quals are available from the index tuple.
+	 */
+	if (index->amcanreturn && !index->predOK && !index->indexprs)
+	{
+		Bitmapset  *index_attrs = NULL;
+		int		i;
+
+		/* If we haven't cached base_attrs_used yet, do so now. */
+		if (rel->base_attrs_used == NULL)
+		{
+			ListCell	*lc;
+
+			/* Add all the attributes needed by joins or final output. */
+			for (i = rel->min_attr; i <= rel->max_attr; i++)
+				if (!bms_is_empty(rel->attr_needed[i - rel->min_attr]))
+					rel->base_attrs_used = bms_add_member(rel->base_attrs_used,
+						i - FirstLowInvalidHeapAttributeNumber);
+
+			/* Add all the attributes used by restrictinfos. */
+			foreach (lc, rel->baserestrictinfo)
+			{
+				RestrictInfo   *rinfo = (RestrictInfo *) lfirst(lc);
+				pull_varattnos((Node *) rinfo->clause, rel->relid, &rel->base_attrs_used);
+			}
+		}
+
+		/* Do we have all the necessary attributes? */
+		for (i = 0; i < index->ncolumns; i++)
+		{
+			int		attno = index->indexkeys[i];
+
+			Assert(attno != 0);
+			index_attrs =
+				bms_add_member(index_attrs,
+							   attno - FirstLowInvalidHeapAttributeNumber);
+		}
+		pathnode->try_index_only_scan =
+			!bms_nonempty_difference(rel->base_attrs_used, index_attrs);
+		bms_free(index_attrs);
+	}
+
 	if (outer_rel != NULL)
 	{
 		/*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 6259170..42bc4cf 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -215,6 +215,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 			info->amsearchnulls = indexRelation->rd_am->amsearchnulls;
 			info->amhasgettuple = OidIsValid(indexRelation->rd_am->amgettuple);
 			info->amhasgetbitmap = OidIsValid(indexRelation->rd_am->amgetbitmap);
+			info->amcanreturn = indexRelation->rd_am->amcanreturn;
 
 			/*
 			 * Fetch the ordering information for the index, if any.
diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c
index 8ce7ee4..2cec150 100644
--- a/src/backend/optimizer/util/var.c
+++ b/src/backend/optimizer/util/var.c
@@ -36,6 +36,12 @@ typedef struct
 
 typedef struct
 {
+	Bitmapset  *varattnos;
+	Index		varno;
+} pull_varattnos_context;
+
+typedef struct
+{
 	int			var_location;
 	int			sublevels_up;
 } locate_var_of_level_context;
@@ -70,7 +76,7 @@ typedef struct
 
 static bool pull_varnos_walker(Node *node,
 				   pull_varnos_context *context);
-static bool pull_varattnos_walker(Node *node, Bitmapset **varattnos);
+static bool pull_varattnos_walker(Node *node, pull_varattnos_context *context);
 static bool contain_var_clause_walker(Node *node, void *context);
 static bool contain_vars_of_level_walker(Node *node, int *sublevels_up);
 static bool locate_var_of_level_walker(Node *node,
@@ -177,41 +183,48 @@ pull_varnos_walker(Node *node, pull_varnos_context *context)
  * pull_varattnos
  *		Find all the distinct attribute numbers present in an expression tree,
  *		and add them to the initial contents of *varattnos.
- *		Only Vars that reference RTE 1 of rtable level zero are considered.
+ *		Only the given varno at rtable level zero is considered.
  *
  * Attribute numbers are offset by FirstLowInvalidHeapAttributeNumber so that
  * we can include system attributes (e.g., OID) in the bitmap representation.
  *
- * Currently, this does not support subqueries nor expressions containing
- * references to multiple tables; not needed since it's only applied to
- * index expressions and predicates.
+ * Currently, this does not support subqueries; this is not needed for current uses.
  */
 void
-pull_varattnos(Node *node, Bitmapset **varattnos)
+pull_varattnos(Node *node, Index varno, Bitmapset **varattnos)
 {
-	(void) pull_varattnos_walker(node, varattnos);
+	pull_varattnos_context context;
+
+	context.varattnos = *varattnos;
+	context.varno = varno;
+
+	(void) pull_varattnos_walker(node, &context);
+
+	*varattnos = context.varattnos;
 }
 
 static bool
-pull_varattnos_walker(Node *node, Bitmapset **varattnos)
+pull_varattnos_walker(Node *node, pull_varattnos_context *context)
 {
+
 	if (node == NULL)
 		return false;
 	if (IsA(node, Var))
 	{
 		Var		   *var = (Var *) node;
 
-		Assert(var->varno == 1);
-		*varattnos = bms_add_member(*varattnos,
-						 var->varattno - FirstLowInvalidHeapAttributeNumber);
+		if (var->varno == context->varno && var->varlevelsup == 0)
+			context->varattnos =
+				bms_add_member(context->varattnos,
+							   var->varattno - FirstLowInvalidHeapAttributeNumber);
 		return false;
 	}
-	/* Should not find a subquery or subplan */
+
+	/* Should not find a subquery */
 	Assert(!IsA(node, Query));
-	Assert(!IsA(node, SubPlan));
 
 	return expression_tree_walker(node, pull_varattnos_walker,
-								  (void *) varattnos);
+								  (void *) context);
 }
 
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 809222b..e4ec953 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -3676,10 +3676,10 @@ RelationGetIndexAttrBitmap(Relation relation)
 		}
 
 		/* Collect all attributes used in expressions, too */
-		pull_varattnos((Node *) indexInfo->ii_Expressions, &indexattrs);
+		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
 
 		/* Collect all attributes in the index predicate, too */
-		pull_varattnos((Node *) indexInfo->ii_Predicate, &indexattrs);
+		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
 
 		index_close(indexDesc, AccessShareLock);
 	}
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index a95b3d7..2250485 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -145,6 +145,9 @@ extern void index_rescan(IndexScanDesc scan,
 extern void index_endscan(IndexScanDesc scan);
 extern void index_markpos(IndexScanDesc scan);
 extern void index_restrpos(IndexScanDesc scan);
+extern ItemPointer index_getnexttid(IndexScanDesc scan,
+				 ScanDirection direction);
+extern HeapTuple index_fetch(IndexScanDesc scan);
 extern HeapTuple index_getnext(IndexScanDesc scan, ScanDirection direction);
 extern int64 index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap);
 
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 57d08b9..d3eb29d 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -16,6 +16,7 @@
 
 #include "access/genam.h"
 #include "access/heapam.h"
+#include "access/itup.h"
 
 
 typedef struct HeapScanDescData
@@ -84,6 +85,10 @@ typedef struct IndexScanDescData
 
 	/* state data for traversing HOT chains in index_getnext */
 	bool		xs_continue_hot;	/* T if must keep walking HOT chain */
+
+	/* state for index-only scans */
+	bool		want_index_tuple;	/* caller requests index tuple from AM */
+	IndexTuple	xs_itup;			/* index tuple returned by AM */
 }	IndexScanDescData;
 
 /* Struct for heap-or-index scans of system tables */
diff --git a/src/include/catalog/pg_am.h b/src/include/catalog/pg_am.h
index feab420..31b2a31 100644
--- a/src/include/catalog/pg_am.h
+++ b/src/include/catalog/pg_am.h
@@ -65,6 +65,7 @@ CATALOG(pg_am,2601)
 	regproc		amvacuumcleanup;	/* post-VACUUM cleanup function */
 	regproc		amcostestimate; /* estimate cost of an indexscan */
 	regproc		amoptions;		/* parse AM-specific parameters */
+	bool		amcanreturn;	/* AM knows how to return tuple or not */
 } FormData_pg_am;
 
 /* ----------------
@@ -78,7 +79,7 @@ typedef FormData_pg_am *Form_pg_am;
  *		compiler constants for pg_am
  * ----------------
  */
-#define Natts_pg_am						28
+#define Natts_pg_am						29
 #define Anum_pg_am_amname				1
 #define Anum_pg_am_amstrategies			2
 #define Anum_pg_am_amsupport			3
@@ -107,22 +108,23 @@ typedef FormData_pg_am *Form_pg_am;
 #define Anum_pg_am_amvacuumcleanup		26
 #define Anum_pg_am_amcostestimate		27
 #define Anum_pg_am_amoptions			28
+#define Anum_pg_am_canreturn			29
 
 /* ----------------
  *		initial contents of pg_am
  * ----------------
  */
 
-DATA(insert OID = 403 (  btree	5 1 t f t t t t t f t t 0 btinsert btbeginscan btgettuple btgetbitmap btrescan btendscan btmarkpos btrestrpos btbuild btbuildempty btbulkdelete btvacuumcleanup btcostestimate btoptions ));
+DATA(insert OID = 403 (  btree	5 1 t f t t t t t f t t 0 btinsert btbeginscan btgettuple btgetbitmap btrescan btendscan btmarkpos btrestrpos btbuild btbuildempty btbulkdelete btvacuumcleanup btcostestimate btoptions t));
 DESCR("b-tree index access method");
 #define BTREE_AM_OID 403
-DATA(insert OID = 405 (  hash	1 1 f f t f f f f f f f 23 hashinsert hashbeginscan hashgettuple hashgetbitmap hashrescan hashendscan hashmarkpos hashrestrpos hashbuild hashbuildempty hashbulkdelete hashvacuumcleanup hashcostestimate hashoptions ));
+DATA(insert OID = 405 (  hash	1 1 f f t f f f f f f f 23 hashinsert hashbeginscan hashgettuple hashgetbitmap hashrescan hashendscan hashmarkpos hashrestrpos hashbuild hashbuildempty hashbulkdelete hashvacuumcleanup hashcostestimate hashoptions f));
 DESCR("hash index access method");
 #define HASH_AM_OID 405
-DATA(insert OID = 783 (  gist	0 8 f t f f t t t t t f 0 gistinsert gistbeginscan gistgettuple gistgetbitmap gistrescan gistendscan gistmarkpos gistrestrpos gistbuild gistbuildempty gistbulkdelete gistvacuumcleanup gistcostestimate gistoptions ));
+DATA(insert OID = 783 (  gist	0 8 f t f f t t t t t f 0 gistinsert gistbeginscan gistgettuple gistgetbitmap gistrescan gistendscan gistmarkpos gistrestrpos gistbuild gistbuildempty gistbulkdelete gistvacuumcleanup gistcostestimate gistoptions f));
 DESCR("GiST index access method");
 #define GIST_AM_OID 783
-DATA(insert OID = 2742 (  gin	0 5 f f f f t t f t f f 0 gininsert ginbeginscan - gingetbitmap ginrescan ginendscan ginmarkpos ginrestrpos ginbuild ginbuildempty ginbulkdelete ginvacuumcleanup gincostestimate ginoptions ));
+DATA(insert OID = 2742 (  gin	0 5 f f f f t t f t f f 0 gininsert ginbeginscan - gingetbitmap ginrescan ginendscan ginmarkpos ginrestrpos ginbuild ginbuildempty ginbulkdelete ginvacuumcleanup gincostestimate ginoptions f));
 DESCR("GIN index access method");
 #define GIN_AM_OID 2742
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a3a9310..4369453 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1217,6 +1217,7 @@ typedef struct
  *		RuntimeContext	   expr context for evaling runtime Skeys
  *		RelationDesc	   index relation descriptor
  *		ScanDesc		   index scan descriptor
+ *		VMBuffer		   buffer pinned for visibility map testing
  * ----------------
  */
 typedef struct IndexScanState
@@ -1233,6 +1234,7 @@ typedef struct IndexScanState
 	ExprContext *iss_RuntimeContext;
 	Relation	iss_RelationDesc;
 	IndexScanDesc iss_ScanDesc;
+	Buffer		iss_VMBuffer;
 } IndexScanState;
 
 /* ----------------
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 7c085b3..feb9d8e 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -312,6 +312,9 @@ typedef struct IndexScan
 	List	   *indexorderby;	/* list of index ORDER BY exprs */
 	List	   *indexorderbyorig;		/* the same in original form */
 	ScanDirection indexorderdir;	/* forward or backward or don't care */
+	bool		try_index_only_scan;	/* attempt to skip heap fetches? */
+	int			nheapatt;		/* number of heap attributes */
+	int		   *heapatt;		/* map from heap attrs to index attrs */
 } IndexScan;
 
 /* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index f659269..eee9797 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -415,6 +415,8 @@ typedef struct RelOptInfo
 	/* used by various scans and joins: */
 	List	   *baserestrictinfo;		/* RestrictInfo structures (if base
 										 * rel) */
+	Bitmapset  *base_attrs_used;		/* all attributes used in any way,
+										 * including in baserestrictinfo */
 	QualCost	baserestrictcost;		/* cost of evaluating the above */
 	List	   *joininfo;		/* RestrictInfo structures for join clauses
 								 * involving this rel */
@@ -491,6 +493,7 @@ typedef struct IndexOptInfo
 	bool		amsearchnulls;	/* can AM search for NULL/NOT NULL entries? */
 	bool		amhasgettuple;	/* does AM have amgettuple interface? */
 	bool		amhasgetbitmap; /* does AM have amgetbitmap interface? */
+	bool		amcanreturn;	/* does AM know how to return tuple? */
 } IndexOptInfo;
 
 
@@ -701,6 +704,7 @@ typedef struct IndexPath
 	Cost		indextotalcost;
 	Selectivity indexselectivity;
 	double		rows;			/* estimated number of result tuples */
+	bool		try_index_only_scan;
 } IndexPath;
 
 /*
diff --git a/src/include/optimizer/var.h b/src/include/optimizer/var.h
index 5d7e2d9..4fd0052 100644
--- a/src/include/optimizer/var.h
+++ b/src/include/optimizer/var.h
@@ -31,7 +31,7 @@ typedef enum
 } PVCPlaceHolderBehavior;
 
 extern Relids pull_varnos(Node *node);
-extern void pull_varattnos(Node *node, Bitmapset **varattnos);
+extern void pull_varattnos(Node *node, Index varno, Bitmapset **varattnos);
 extern bool contain_var_clause(Node *node);
 extern bool contain_vars_of_level(Node *node, int levelsup);
 extern int	locate_var_of_level(Node *node, int levelsup);
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 4861006..43b5e38 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -449,12 +449,12 @@ analyze tenk1;		-- ensure we get consistent plans here
 -- Basic cases
 explain (costs off)
   select min(unique1) from tenk1;
-                      QUERY PLAN                       
--------------------------------------------------------
+                         QUERY PLAN                         
+------------------------------------------------------------
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan using tenk1_unique1 on tenk1
+           ->  Index Only Scan using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
 (5 rows)
 
@@ -466,12 +466,12 @@ select min(unique1) from tenk1;
 
 explain (costs off)
   select max(unique1) from tenk1;
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique1 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: (unique1 IS NOT NULL)
 (5 rows)
 
@@ -488,7 +488,7 @@ explain (costs off)
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique1 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42))
 (5 rows)
 
@@ -505,7 +505,7 @@ explain (costs off)
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique1 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42))
 (5 rows)
 
@@ -522,7 +522,7 @@ explain (costs off)
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique1 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique1 on tenk1
                  Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000))
 (5 rows)
 
@@ -535,12 +535,12 @@ select max(unique1) from tenk1 where unique1 > 42000;
 -- multi-column index (uses tenk1_thous_tenthous)
 explain (costs off)
   select max(tenthous) from tenk1 where thousand = 33;
-                                QUERY PLAN                                
---------------------------------------------------------------------------
+                                 QUERY PLAN                                 
+----------------------------------------------------------------------------
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_thous_tenthous on tenk1
+           ->  Index Only Scan Backward using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
 (5 rows)
 
@@ -557,7 +557,7 @@ explain (costs off)
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan using tenk1_thous_tenthous on tenk1
+           ->  Index Only Scan using tenk1_thous_tenthous on tenk1
                  Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL))
 (5 rows)
 
@@ -578,7 +578,7 @@ explain (costs off)
      ->  Result
            InitPlan 1 (returns $1)
              ->  Limit
-                   ->  Index Scan using tenk1_unique1 on tenk1
+                   ->  Index Only Scan using tenk1_unique1 on tenk1
                          Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1))
 (7 rows)
 
@@ -596,12 +596,12 @@ select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt
 -- check some cases that were handled incorrectly in 8.3.0
 explain (costs off)
   select distinct max(unique2) from tenk1;
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  HashAggregate
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique2 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
 (6 rows)
@@ -614,13 +614,13 @@ select distinct max(unique2) from tenk1;
 
 explain (costs off)
   select max(unique2) from tenk1 order by 1;
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Sort
    Sort Key: ($0)
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique2 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
 (7 rows)
@@ -633,13 +633,13 @@ select max(unique2) from tenk1 order by 1;
 
 explain (costs off)
   select max(unique2) from tenk1 order by max(unique2);
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Sort
    Sort Key: ($0)
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique2 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
 (7 rows)
@@ -652,13 +652,13 @@ select max(unique2) from tenk1 order by max(unique2);
 
 explain (costs off)
   select max(unique2) from tenk1 order by max(unique2)+1;
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Sort
    Sort Key: (($0 + 1))
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique2 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
 (7 rows)
@@ -671,13 +671,13 @@ select max(unique2) from tenk1 order by max(unique2)+1;
 
 explain (costs off)
   select max(unique2), generate_series(1,3) as g from tenk1 order by g desc;
-                           QUERY PLAN                           
-----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Sort
    Sort Key: (generate_series(1, 3))
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Index Scan Backward using tenk1_unique2 on tenk1
+           ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
 (7 rows)
@@ -705,18 +705,18 @@ insert into minmaxtest2 values(15), (16);
 insert into minmaxtest3 values(17), (18);
 explain (costs off)
   select min(f1), max(f1) from minmaxtest;
-                                      QUERY PLAN                                      
---------------------------------------------------------------------------------------
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
  Result
    InitPlan 1 (returns $0)
      ->  Limit
            ->  Merge Append
                  Sort Key: public.minmaxtest.f1
-                 ->  Index Scan using minmaxtesti on minmaxtest
+                 ->  Index Only Scan using minmaxtesti on minmaxtest
                        Index Cond: (f1 IS NOT NULL)
-                 ->  Index Scan using minmaxtest1i on minmaxtest1 minmaxtest
+                 ->  Index Only Scan using minmaxtest1i on minmaxtest1 minmaxtest
                        Index Cond: (f1 IS NOT NULL)
-                 ->  Index Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest
+                 ->  Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Scan using minmaxtest3i on minmaxtest3 minmaxtest
                        Index Cond: (f1 IS NOT NULL)
@@ -724,11 +724,11 @@ explain (costs off)
      ->  Limit
            ->  Merge Append
                  Sort Key: public.minmaxtest.f1
-                 ->  Index Scan Backward using minmaxtesti on minmaxtest
+                 ->  Index Only Scan Backward using minmaxtesti on minmaxtest
                        Index Cond: (f1 IS NOT NULL)
-                 ->  Index Scan Backward using minmaxtest1i on minmaxtest1 minmaxtest
+                 ->  Index Only Scan Backward using minmaxtest1i on minmaxtest1 minmaxtest
                        Index Cond: (f1 IS NOT NULL)
-                 ->  Index Scan using minmaxtest2i on minmaxtest2 minmaxtest
+                 ->  Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest
                        Index Cond: (f1 IS NOT NULL)
                  ->  Index Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest
                        Index Cond: (f1 IS NOT NULL)