*** a/src/backend/access/nbtree/README
--- b/src/backend/access/nbtree/README
***************
*** 92,108 **** To minimize lock/unlock traffic, an index scan always searches a leaf page
  to identify all the matching items at once, copying their heap tuple IDs
  into backend-local storage.  The heap tuple IDs are then processed while
  not holding any page lock within the index.  We do continue to hold a pin
! on the leaf page, to protect against concurrent deletions (see below).
! In this state the scan is effectively stopped "between" pages, either
! before or after the page it has pinned.  This is safe in the presence of
! concurrent insertions and even page splits, because items are never moved
! across pre-existing page boundaries --- so the scan cannot miss any items
! it should have seen, nor accidentally return the same item twice.  The scan
! must remember the page's right-link at the time it was scanned, since that
! is the page to move right to; if we move right to the current right-link
! then we'd re-scan any items moved by a page split.  We don't similarly
! remember the left-link, since it's best to use the most up-to-date
! left-link when trying to move left (see detailed move-left algorithm below).
  
  In most cases we release our lock and pin on a page before attempting
  to acquire pin and lock on the page we are moving to.  In a few places
--- 92,109 ----
  to identify all the matching items at once, copying their heap tuple IDs
  into backend-local storage.  The heap tuple IDs are then processed while
  not holding any page lock within the index.  We do continue to hold a pin
! on the leaf page in some circumstances, to protect against concurrent
! deletions (see below).  In this state the scan is effectively stopped
! "between" pages, either before or after the page it has pinned.  This is
! safe in the presence of concurrent insertions and even page splits, because
! items are never moved across pre-existing page boundaries --- so the scan
! cannot miss any items it should have seen, nor accidentally return the same
! item twice.  The scan must remember the page's right-link at the time it
! was scanned, since that is the page to move right to; if we move right to
! the current right-link then we'd re-scan any items moved by a page split.
! We don't similarly remember the left-link, since it's best to use the most
! up-to-date left-link when trying to move left (see detailed move-left
! algorithm below).
  
  In most cases we release our lock and pin on a page before attempting
  to acquire pin and lock on the page we are moving to.  In a few places
***************
*** 154,178 **** starts.  This is not necessary for correctness in terms of the btree index
  operations themselves; as explained above, index scans logically stop
  "between" pages and so can't lose their place.  The reason we do it is to
  provide an interlock between non-full VACUUM and indexscans.  Since VACUUM
! deletes index entries before deleting tuples, the super-exclusive lock
! guarantees that VACUUM can't delete any heap tuple that an indexscanning
! process might be about to visit.  (This guarantee works only for simple
! indexscans that visit the heap in sync with the index scan, not for bitmap
! scans.  We only need the guarantee when using non-MVCC snapshot rules; in
! an MVCC snapshot, it wouldn't matter if the heap tuple were replaced with
! an unrelated tuple at the same TID, because the new tuple wouldn't be
! visible to our scan anyway.)
! 
! Because a page can be split even while someone holds a pin on it, it is
! possible that an indexscan will return items that are no longer stored on
! the page it has a pin on, but rather somewhere to the right of that page.
! To ensure that VACUUM can't prematurely remove such heap tuples, we require
! btbulkdelete to obtain super-exclusive lock on every leaf page in the index,
! even pages that don't contain any deletable tuples.  This guarantees that
! the btbulkdelete call cannot return while any indexscan is still holding
! a copy of a deleted index tuple.  Note that this requirement does not say
! that btbulkdelete must visit the pages in any particular order.  (See also
! on-the-fly deletion, below.)
  
  There is no such interlocking for deletion of items in internal pages,
  since backends keep no lock nor pin on a page they have descended past.
--- 155,186 ----
  operations themselves; as explained above, index scans logically stop
  "between" pages and so can't lose their place.  The reason we do it is to
  provide an interlock between non-full VACUUM and indexscans.  Since VACUUM
! deletes index entries before reclaiming heap tuple line pointers, the
! super-exclusive lock guarantees that VACUUM can't reclaim for re-use a
! line pointer that an indexscanning process might be about to visit.  This
! guarantee works only for simple indexscans that visit the heap in sync
! with the index scan, not for bitmap scans.  We only need the guarantee
! when using non-MVCC snapshot rules; when using an MVCC snapshot, it
! doesn't matter if the heap tuple is replaced with an unrelated tuple at
! the same TID, because the new tuple won't be visible to our scan anyway.
! Therefore, a scan using an MVCC snapshot which has no other confounding
! factors will not hold the pin after the page contents are read.
! 
! Because a pin is not always held, and a page can be split even while
! someone does hold a pin on it, it is possible that an indexscan will
! return items that are no longer stored on the page it has a pin on, but
! rather somewhere to the right of that page.  To ensure that VACUUM can't
! prematurely remove such heap tuples, we require btbulkdelete to obtain a
! super-exclusive lock on every leaf page in the index, even pages that
! don't contain any deletable tuples.  Any scan which could yield incorrect
! results if the tuple at a TID matching the the scan's range and filter
! conditions were replaced by a different tuple while the scan is in
! progress must hold the pin on each index page until all index entries read
! from the page have been processed.  This guarantees that the btbulkdelete
! call cannot return while any indexscan is still holding a copy of a
! deleted index tuple if the scan could be confused by that.  Note that this
! requirement does not say that btbulkdelete must visit the pages in any
! particular order.  (See also on-the-fly deletion, below.)
  
  There is no such interlocking for deletion of items in internal pages,
  since backends keep no lock nor pin on a page they have descended past.
***************
*** 397,403 **** not so: as long as an indexscanning process has a pin on the page where
  the index item used to be, VACUUM cannot complete its btbulkdelete scan
  and so cannot remove the heap tuple.  This is another reason why
  btbulkdelete has to get super-exclusive lock on every leaf page, not only
! the ones where it actually sees items to delete.
  
  WAL Considerations
  ------------------
--- 405,415 ----
  the index item used to be, VACUUM cannot complete its btbulkdelete scan
  and so cannot remove the heap tuple.  This is another reason why
  btbulkdelete has to get super-exclusive lock on every leaf page, not only
! the ones where it actually sees items to delete.  To handle the cases
! where it is safe to release the pin after reading the index page, the LSN
! of the index page is read along with the index entries before the shared
! lock is released, and we do not attempt to flag index tuples as dead if
! the page is not pinned and the LSN does not match.
  
  WAL Considerations
  ------------------
***************
*** 462,468 **** metapage update (of the "fast root" link) is performed and logged as part
  of the insertion into the parent level.  When splitting the root page, the
  metapage update is handled as part of the "new root" action.
  
! Each step in page deletion are logged as separate WAL entries: marking the
  leaf as half-dead and removing the downlink is one record, and unlinking a
  page is a second record.  If vacuum is interrupted for some reason, or the
  system crashes, the tree is consistent for searches and insertions.  The
--- 474,480 ----
  of the insertion into the parent level.  When splitting the root page, the
  metapage update is handled as part of the "new root" action.
  
! Each step in page deletion is logged as a separate WAL entry: marking the
  leaf as half-dead and removing the downlink is one record, and unlinking a
  page is a second record.  If vacuum is interrupted for some reason, or the
  system crashes, the tree is consistent for searches and insertions.  The
*** a/src/backend/access/nbtree/nbtinsert.c
--- b/src/backend/access/nbtree/nbtinsert.c
***************
*** 498,506 **** _bt_check_unique(Relation rel, IndexTuple itup, Relation heapRel,
   *		If there's not enough room in the space, we try to make room by
   *		removing any LP_DEAD tuples.
   *
!  *		On entry, *buf and *offsetptr point to the first legal position
   *		where the new tuple could be inserted.  The caller should hold an
!  *		exclusive lock on *buf.  *offsetptr can also be set to
   *		InvalidOffsetNumber, in which case the function will search for the
   *		right location within the page if needed.  On exit, they point to the
   *		chosen insert location.  If _bt_findinsertloc decides to move right,
--- 498,506 ----
   *		If there's not enough room in the space, we try to make room by
   *		removing any LP_DEAD tuples.
   *
!  *		On entry, *bufptr and *offsetptr point to the first legal position
   *		where the new tuple could be inserted.  The caller should hold an
!  *		exclusive lock on *bufptr.  *offsetptr can also be set to
   *		InvalidOffsetNumber, in which case the function will search for the
   *		right location within the page if needed.  On exit, they point to the
   *		chosen insert location.  If _bt_findinsertloc decides to move right,
*** a/src/backend/access/nbtree/nbtree.c
--- b/src/backend/access/nbtree/nbtree.c
***************
*** 404,410 **** btbeginscan(PG_FUNCTION_ARGS)
  
  	/* allocate private workspace */
  	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
! 	so->currPos.buf = so->markPos.buf = InvalidBuffer;
  	if (scan->numberOfKeys > 0)
  		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
  	else
--- 404,411 ----
  
  	/* allocate private workspace */
  	so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData));
! 	BTScanPosInvalidate(so->currPos);
! 	BTScanPosInvalidate(so->markPos);
  	if (scan->numberOfKeys > 0)
  		so->keyData = (ScanKey) palloc(scan->numberOfKeys * sizeof(ScanKeyData));
  	else
***************
*** 424,431 **** btbeginscan(PG_FUNCTION_ARGS)
  	 * scan->xs_itupdesc whether we'll need it or not, since that's so cheap.
  	 */
  	so->currTuples = so->markTuples = NULL;
- 	so->currPos.nextTupleOffset = 0;
- 	so->markPos.nextTupleOffset = 0;
  
  	scan->xs_itupdesc = RelationGetDescr(rel);
  
--- 425,430 ----
***************
*** 451,467 **** btrescan(PG_FUNCTION_ARGS)
  	{
  		/* Before leaving current page, deal with any killed items */
  		if (so->numKilled > 0)
! 			_bt_killitems(scan, false);
! 		ReleaseBuffer(so->currPos.buf);
! 		so->currPos.buf = InvalidBuffer;
  	}
  
- 	if (BTScanPosIsValid(so->markPos))
- 	{
- 		ReleaseBuffer(so->markPos.buf);
- 		so->markPos.buf = InvalidBuffer;
- 	}
  	so->markItemIndex = -1;
  
  	/*
  	 * Allocate tuple workspace arrays, if needed for an index-only scan and
--- 450,463 ----
  	{
  		/* Before leaving current page, deal with any killed items */
  		if (so->numKilled > 0)
! 			_bt_killitems(scan);
! 		BTScanPosUnpinIfPinned(so->currPos);
! 		BTScanPosInvalidate(so->currPos);
  	}
  
  	so->markItemIndex = -1;
+ 	BTScanPosUnpinIfPinned(so->markPos);
+ 	BTScanPosInvalidate(so->markPos);
  
  	/*
  	 * Allocate tuple workspace arrays, if needed for an index-only scan and
***************
*** 515,531 **** btendscan(PG_FUNCTION_ARGS)
  	{
  		/* Before leaving current page, deal with any killed items */
  		if (so->numKilled > 0)
! 			_bt_killitems(scan, false);
! 		ReleaseBuffer(so->currPos.buf);
! 		so->currPos.buf = InvalidBuffer;
  	}
  
- 	if (BTScanPosIsValid(so->markPos))
- 	{
- 		ReleaseBuffer(so->markPos.buf);
- 		so->markPos.buf = InvalidBuffer;
- 	}
  	so->markItemIndex = -1;
  
  	/* Release storage */
  	if (so->keyData != NULL)
--- 511,524 ----
  	{
  		/* Before leaving current page, deal with any killed items */
  		if (so->numKilled > 0)
! 			_bt_killitems(scan);
! 		BTScanPosUnpinIfPinned(so->currPos);
  	}
  
  	so->markItemIndex = -1;
+ 	BTScanPosUnpinIfPinned(so->markPos);
+ 
+ 	/* No need to invalidate positions, the RAM is about to be freed. */
  
  	/* Release storage */
  	if (so->keyData != NULL)
***************
*** 552,563 **** btmarkpos(PG_FUNCTION_ARGS)
  	IndexScanDesc scan = (IndexScanDesc) PG_GETARG_POINTER(0);
  	BTScanOpaque so = (BTScanOpaque) scan->opaque;
  
! 	/* we aren't holding any read locks, but gotta drop the pin */
! 	if (BTScanPosIsValid(so->markPos))
! 	{
! 		ReleaseBuffer(so->markPos.buf);
! 		so->markPos.buf = InvalidBuffer;
! 	}
  
  	/*
  	 * Just record the current itemIndex.  If we later step to next page
--- 545,552 ----
  	IndexScanDesc scan = (IndexScanDesc) PG_GETARG_POINTER(0);
  	BTScanOpaque so = (BTScanOpaque) scan->opaque;
  
! 	/* There may be an old mark with a pin (but no lock). */
! 	BTScanPosUnpinIfPinned(so->markPos);
  
  	/*
  	 * Just record the current itemIndex.  If we later step to next page
***************
*** 568,574 **** btmarkpos(PG_FUNCTION_ARGS)
--- 557,566 ----
  	if (BTScanPosIsValid(so->currPos))
  		so->markItemIndex = so->currPos.itemIndex;
  	else
+ 	{
+ 		BTScanPosInvalidate(so->markPos);
  		so->markItemIndex = -1;
+ 	}
  
  	/* Also record the current positions of any array keys */
  	if (so->numArrayKeys)
***************
*** 600,620 **** btrestrpos(PG_FUNCTION_ARGS)
  	}
  	else
  	{
! 		/* we aren't holding any read locks, but gotta drop the pin */
  		if (BTScanPosIsValid(so->currPos))
  		{
  			/* Before leaving current page, deal with any killed items */
  			if (so->numKilled > 0 &&
! 				so->currPos.buf != so->markPos.buf)
! 				_bt_killitems(scan, false);
! 			ReleaseBuffer(so->currPos.buf);
! 			so->currPos.buf = InvalidBuffer;
  		}
  
  		if (BTScanPosIsValid(so->markPos))
  		{
  			/* bump pin on mark buffer for assignment to current buffer */
! 			IncrBufferRefCount(so->markPos.buf);
  			memcpy(&so->currPos, &so->markPos,
  				   offsetof(BTScanPosData, items[1]) +
  				   so->markPos.lastItem * sizeof(BTScanPosItem));
--- 592,623 ----
  	}
  	else
  	{
! 		/*
! 		 * The scan moved to a new page after the mark, and we are now
! 		 * restoring to the marked page.  We aren't holding any read locks,
! 		 * but if we're still holding the pin for the current position, we
! 		 * must drop it.
! 		 *
! 		 * NB: This must deal with the possibility that so->markItemIndex < 0
! 		 * but so->currPos == so->markPos.  This would be an unusual case,
! 		 * where the scan moved to a new index page after the mark, restored,
! 		 * and later restored again without setting a new mark or moving off
! 		 * the page again.
! 		 */
  		if (BTScanPosIsValid(so->currPos))
  		{
  			/* Before leaving current page, deal with any killed items */
  			if (so->numKilled > 0 &&
! 				so->currPos.currPage != so->markPos.currPage)
! 				_bt_killitems(scan);
! 			BTScanPosUnpinIfPinned(so->currPos);
  		}
  
  		if (BTScanPosIsValid(so->markPos))
  		{
  			/* bump pin on mark buffer for assignment to current buffer */
! 			if (BTScanPosIsPinned(so->markPos))
! 				IncrBufferRefCount(so->markPos.buf);
  			memcpy(&so->currPos, &so->markPos,
  				   offsetof(BTScanPosData, items[1]) +
  				   so->markPos.lastItem * sizeof(BTScanPosItem));
***************
*** 622,627 **** btrestrpos(PG_FUNCTION_ARGS)
--- 625,632 ----
  				memcpy(so->currTuples, so->markTuples,
  					   so->markPos.nextTupleOffset);
  		}
+ 		else
+ 			BTScanPosInvalidate(so->currPos);
  	}
  
  	PG_RETURN_VOID();
*** a/src/backend/access/nbtree/nbtsearch.c
--- b/src/backend/access/nbtree/nbtsearch.c
***************
*** 22,27 ****
--- 22,28 ----
  #include "storage/predicate.h"
  #include "utils/lsyscache.h"
  #include "utils/rel.h"
+ #include "utils/tqual.h"
  
  
  static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir,
***************
*** 31,36 **** static void _bt_saveitem(BTScanOpaque so, int itemIndex,
--- 32,67 ----
  static bool _bt_steppage(IndexScanDesc scan, ScanDirection dir);
  static Buffer _bt_walk_left(Relation rel, Buffer buf);
  static bool _bt_endpoint(IndexScanDesc scan, ScanDirection dir);
+ static void _bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp);
+ 
+ 
+ /*
+  *	_bt_drop_lock_and_maybe_pin()
+  *
+  * Unlock the buffer; and if it is safe to release the pin, do that, too.  It
+  * is safe if the scan is using an MVCC snapshot and the index is WAL-logged.
+  * This will prevent vacuum from stalling in a blocked state trying to read a
+  * page when a cursor is sitting on it -- at least in many important cases.
+  *
+  * Set the buffer to invalid if the pin is released, since the buffer may be
+  * re-used.  If we need to go back to this block (for example, to apply
+  * LP_DEAD hints) we must get a fresh reference to the buffer.  Hopefully it
+  * will remain in shared memory for as long as it takes to scan the index
+  * buffer page.
+  */
+ static void
+ _bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp)
+ {
+ 	LockBuffer(sp->buf, BUFFER_LOCK_UNLOCK);
+ 
+ 	if (IsMVCCSnapshot(scan->xs_snapshot) &&
+ 		RelationNeedsWAL(scan->indexRelation) &&
+ 		!scan->xs_want_itup)
+ 	{
+ 		ReleaseBuffer(sp->buf);
+ 		sp->buf = InvalidBuffer;
+ 	}
+ }
  
  
  /*
***************
*** 506,511 **** _bt_first(IndexScanDesc scan, ScanDirection dir)
--- 537,544 ----
  	StrategyNumber strat_total;
  	BTScanPosItem *currItem;
  
+ 	Assert(!BTScanPosIsValid(so->currPos));
+ 
  	pgstat_count_index_scan(rel);
  
  	/*
***************
*** 942,950 **** _bt_first(IndexScanDesc scan, ScanDirection dir)
  	/* don't need to keep the stack around... */
  	_bt_freestack(stack);
  
- 	/* remember which buffer we have pinned, if any */
- 	so->currPos.buf = buf;
- 
  	if (!BufferIsValid(buf))
  	{
  		/*
--- 975,980 ----
***************
*** 1023,1028 **** _bt_first(IndexScanDesc scan, ScanDirection dir)
--- 1053,1062 ----
  			break;
  	}
  
+ 	/* remember which buffer we have pinned, if any */
+ 	Assert(!BTScanPosIsValid(so->currPos));
+ 	so->currPos.buf = buf;
+ 
  	/*
  	 * Now load data from the first page of the scan.
  	 */
***************
*** 1032,1043 **** _bt_first(IndexScanDesc scan, ScanDirection dir)
  		 * There's no actually-matching data on this page.  Try to advance to
  		 * the next page.  Return false if there's no matching data at all.
  		 */
  		if (!_bt_steppage(scan, dir))
  			return false;
  	}
! 
! 	/* Drop the lock, but not pin, on the current page */
! 	LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  
  	/* OK, itemIndex says what to return */
  	currItem = &so->currPos.items[so->currPos.itemIndex];
--- 1066,1080 ----
  		 * There's no actually-matching data on this page.  Try to advance to
  		 * the next page.  Return false if there's no matching data at all.
  		 */
+ 		LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  		if (!_bt_steppage(scan, dir))
  			return false;
  	}
! 	else
! 	{
! 		/* Drop the lock, and maybe the pin, on the current page */
! 		_bt_drop_lock_and_maybe_pin(scan, &so->currPos);
! 	}
  
  	/* OK, itemIndex says what to return */
  	currItem = &so->currPos.items[so->currPos.itemIndex];
***************
*** 1051,1058 **** _bt_first(IndexScanDesc scan, ScanDirection dir)
  /*
   *	_bt_next() -- Get the next item in a scan.
   *
!  *		On entry, so->currPos describes the current page, which is pinned
!  *		but not locked, and so->currPos.itemIndex identifies which item was
   *		previously returned.
   *
   *		On successful exit, scan->xs_ctup.t_self is set to the TID of the
--- 1088,1095 ----
  /*
   *	_bt_next() -- Get the next item in a scan.
   *
!  *		On entry, so->currPos describes the current page, which may be pinned
!  *		but is not locked, and so->currPos.itemIndex identifies which item was
   *		previously returned.
   *
   *		On successful exit, scan->xs_ctup.t_self is set to the TID of the
***************
*** 1076,1101 **** _bt_next(IndexScanDesc scan, ScanDirection dir)
  	{
  		if (++so->currPos.itemIndex > so->currPos.lastItem)
  		{
- 			/* We must acquire lock before applying _bt_steppage */
- 			Assert(BufferIsValid(so->currPos.buf));
- 			LockBuffer(so->currPos.buf, BT_READ);
  			if (!_bt_steppage(scan, dir))
  				return false;
- 			/* Drop the lock, but not pin, on the new page */
- 			LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  		}
  	}
  	else
  	{
  		if (--so->currPos.itemIndex < so->currPos.firstItem)
  		{
- 			/* We must acquire lock before applying _bt_steppage */
- 			Assert(BufferIsValid(so->currPos.buf));
- 			LockBuffer(so->currPos.buf, BT_READ);
  			if (!_bt_steppage(scan, dir))
  				return false;
- 			/* Drop the lock, but not pin, on the new page */
- 			LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  		}
  	}
  
--- 1113,1128 ----
***************
*** 1135,1141 **** _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum)
  	IndexTuple	itup;
  	bool		continuescan;
  
! 	/* we must have the buffer pinned and locked */
  	Assert(BufferIsValid(so->currPos.buf));
  
  	page = BufferGetPage(so->currPos.buf);
--- 1162,1171 ----
  	IndexTuple	itup;
  	bool		continuescan;
  
! 	/*
! 	 * We must have the buffer pinned and locked, but the usual macro can't be
! 	 * used here; this function is what makes it good for currPos.
! 	 */
  	Assert(BufferIsValid(so->currPos.buf));
  
  	page = BufferGetPage(so->currPos.buf);
***************
*** 1144,1149 **** _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum)
--- 1174,1192 ----
  	maxoff = PageGetMaxOffsetNumber(page);
  
  	/*
+ 	 * We note the buffer's block number so that we can release the pin later.
+ 	 * This allows us to re-read the buffer if it is needed again for hinting.
+ 	 */
+ 	so->currPos.currPage = BufferGetBlockNumber(so->currPos.buf);
+ 
+ 	/*
+ 	 * We save the LSN of the page as we read it, so that we know whether it
+ 	 * safe to apply LP_DEAD hints to the page later.  This allows us to drop
+ 	 * the pin for MVCC scans, which allows vacuum to avoid blocking.
+ 	 */
+ 	so->currPos.lsn = PageGetLSN(page);
+ 
+ 	/*
  	 * we must save the page's right-link while scanning it; this tells us
  	 * where to step right to after we're done with these items.  There is no
  	 * corresponding need for the left-link, since splits always go right.
***************
*** 1153,1158 **** _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum)
--- 1196,1207 ----
  	/* initialize tuple workspace to empty */
  	so->currPos.nextTupleOffset = 0;
  
+ 	/*
+ 	 * Now that the current page has been made consistent, the macro should be
+ 	 * good.
+ 	 */
+ 	Assert(BTScanPosIsPinned(so->currPos));
+ 
  	if (ScanDirectionIsForward(dir))
  	{
  		/* load items[] in ascending order */
***************
*** 1241,1251 **** _bt_saveitem(BTScanOpaque so, int itemIndex,
  /*
   *	_bt_steppage() -- Step to next page containing valid data for scan
   *
!  * On entry, so->currPos.buf must be pinned and read-locked.  We'll drop
!  * the lock and pin before moving to next page.
   *
!  * On success exit, we hold pin and read-lock on the next interesting page,
!  * and so->currPos is updated to contain data from that page.
   *
   * If there are no more matching records in the given direction, we drop all
   * locks and pins, set so->currPos.buf to InvalidBuffer, and return FALSE.
--- 1290,1303 ----
  /*
   *	_bt_steppage() -- Step to next page containing valid data for scan
   *
!  * On entry, if so->currPos.buf is valid the buffer is pinned but not locked;
!  * if pinned, we'll drop the pin before moving to next page.  The buffer is
!  * not locked on entry.
   *
!  * On success exit, so->currPos is updated to contain data from the next
!  * interesting page.  For success on a scan using a non-MVCC snapshot we hold
!  * a pin, but not a read lock, on that page.  If we do not hold the pin, we
!  * set so->currPos.buf to InvalidBuffer.  We return TRUE to indicate success.
   *
   * If there are no more matching records in the given direction, we drop all
   * locks and pins, set so->currPos.buf to InvalidBuffer, and return FALSE.
***************
*** 1258,1269 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
  	Page		page;
  	BTPageOpaque opaque;
  
! 	/* we must have the buffer pinned and locked */
! 	Assert(BufferIsValid(so->currPos.buf));
  
  	/* Before leaving current page, deal with any killed items */
  	if (so->numKilled > 0)
! 		_bt_killitems(scan, true);
  
  	/*
  	 * Before we modify currPos, make a copy of the page data if there was a
--- 1310,1320 ----
  	Page		page;
  	BTPageOpaque opaque;
  
! 	Assert(BTScanPosIsValid(so->currPos));
  
  	/* Before leaving current page, deal with any killed items */
  	if (so->numKilled > 0)
! 		_bt_killitems(scan);
  
  	/*
  	 * Before we modify currPos, make a copy of the page data if there was a
***************
*** 1272,1278 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
  	if (so->markItemIndex >= 0)
  	{
  		/* bump pin on current buffer for assignment to mark buffer */
! 		IncrBufferRefCount(so->currPos.buf);
  		memcpy(&so->markPos, &so->currPos,
  			   offsetof(BTScanPosData, items[1]) +
  			   so->currPos.lastItem * sizeof(BTScanPosItem));
--- 1323,1330 ----
  	if (so->markItemIndex >= 0)
  	{
  		/* bump pin on current buffer for assignment to mark buffer */
! 		if (BTScanPosIsPinned(so->currPos))
! 			IncrBufferRefCount(so->currPos.buf);
  		memcpy(&so->markPos, &so->currPos,
  			   offsetof(BTScanPosData, items[1]) +
  			   so->currPos.lastItem * sizeof(BTScanPosItem));
***************
*** 1294,1307 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
  		/* Remember we left a page with data */
  		so->currPos.moreLeft = true;
  
  		for (;;)
  		{
- 			/* release the previous buffer */
- 			_bt_relbuf(rel, so->currPos.buf);
- 			so->currPos.buf = InvalidBuffer;
  			/* if we're at end of scan, give up */
  			if (blkno == P_NONE || !so->currPos.moreRight)
  				return false;
  			/* check for interrupts while we're not holding any buffer lock */
  			CHECK_FOR_INTERRUPTS();
  			/* step right one page */
--- 1346,1362 ----
  		/* Remember we left a page with data */
  		so->currPos.moreLeft = true;
  
+ 		/* release the previous buffer, if pinned */
+ 		BTScanPosUnpinIfPinned(so->currPos);
+ 
  		for (;;)
  		{
  			/* if we're at end of scan, give up */
  			if (blkno == P_NONE || !so->currPos.moreRight)
+ 			{
+ 				BTScanPosInvalidate(so->currPos);
  				return false;
+ 			}
  			/* check for interrupts while we're not holding any buffer lock */
  			CHECK_FOR_INTERRUPTS();
  			/* step right one page */
***************
*** 1317,1324 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
--- 1372,1381 ----
  				if (_bt_readpage(scan, dir, P_FIRSTDATAKEY(opaque)))
  					break;
  			}
+ 
  			/* nope, keep going */
  			blkno = opaque->btpo_next;
+ 			_bt_relbuf(rel, so->currPos.buf);
  		}
  	}
  	else
***************
*** 1326,1331 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
--- 1383,1394 ----
  		/* Remember we left a page with data */
  		so->currPos.moreRight = true;
  
+ 		/* XXX: Can walking left be lighter on the locking and pins? */
+ 		if (BTScanPosIsPinned(so->currPos))
+ 			LockBuffer(so->currPos.buf, BUFFER_LOCK_SHARE);
+ 		else
+ 			so->currPos.buf = _bt_getbuf(rel, so->currPos.currPage, BT_READ);
+ 
  		/*
  		 * Walk left to the next page with data.  This is much more complex
  		 * than the walk-right case because of the possibility that the page
***************
*** 1339,1345 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
  			if (!so->currPos.moreLeft)
  			{
  				_bt_relbuf(rel, so->currPos.buf);
! 				so->currPos.buf = InvalidBuffer;
  				return false;
  			}
  
--- 1402,1408 ----
  			if (!so->currPos.moreLeft)
  			{
  				_bt_relbuf(rel, so->currPos.buf);
! 				BTScanPosInvalidate(so->currPos);
  				return false;
  			}
  
***************
*** 1348,1354 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
--- 1411,1420 ----
  
  			/* if we're physically at end of index, return failure */
  			if (so->currPos.buf == InvalidBuffer)
+ 			{
+ 				BTScanPosInvalidate(so->currPos);
  				return false;
+ 			}
  
  			/*
  			 * Okay, we managed to move left to a non-deleted page. Done if
***************
*** 1368,1373 **** _bt_steppage(IndexScanDesc scan, ScanDirection dir)
--- 1434,1442 ----
  		}
  	}
  
+ 	/* Drop the lock, and maybe the pin, on the current page */
+ 	_bt_drop_lock_and_maybe_pin(scan, &so->currPos);
+ 
  	return true;
  }
  
***************
*** 1604,1610 **** _bt_endpoint(IndexScanDesc scan, ScanDirection dir)
  		 * exists.
  		 */
  		PredicateLockRelation(rel, scan->xs_snapshot);
! 		so->currPos.buf = InvalidBuffer;
  		return false;
  	}
  
--- 1673,1679 ----
  		 * exists.
  		 */
  		PredicateLockRelation(rel, scan->xs_snapshot);
! 		BTScanPosInvalidate(so->currPos);
  		return false;
  	}
  
***************
*** 1658,1669 **** _bt_endpoint(IndexScanDesc scan, ScanDirection dir)
  		 * There's no actually-matching data on this page.  Try to advance to
  		 * the next page.  Return false if there's no matching data at all.
  		 */
  		if (!_bt_steppage(scan, dir))
  			return false;
  	}
! 
! 	/* Drop the lock, but not pin, on the current page */
! 	LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  
  	/* OK, itemIndex says what to return */
  	currItem = &so->currPos.items[so->currPos.itemIndex];
--- 1727,1741 ----
  		 * There's no actually-matching data on this page.  Try to advance to
  		 * the next page.  Return false if there's no matching data at all.
  		 */
+ 		LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  		if (!_bt_steppage(scan, dir))
  			return false;
  	}
! 	else
! 	{
! 		/* Drop the lock, and maybe the pin, on the current page */
! 		_bt_drop_lock_and_maybe_pin(scan, &so->currPos);
! 	}
  
  	/* OK, itemIndex says what to return */
  	currItem = &so->currPos.items[so->currPos.itemIndex];
*** a/src/backend/access/nbtree/nbtutils.c
--- b/src/backend/access/nbtree/nbtutils.c
***************
*** 1721,1747 **** _bt_check_rowcompare(ScanKey skey, IndexTuple tuple, TupleDesc tupdesc,
   * _bt_killitems - set LP_DEAD state for items an indexscan caller has
   * told us were killed
   *
!  * scan->so contains information about the current page and killed tuples
!  * thereon (generally, this should only be called if so->numKilled > 0).
   *
!  * The caller must have pin on so->currPos.buf, but may or may not have
!  * read-lock, as indicated by haveLock.  Note that we assume read-lock
!  * is sufficient for setting LP_DEAD status (which is only a hint).
   *
   * We match items by heap TID before assuming they are the right ones to
   * delete.  We cope with cases where items have moved right due to insertions.
   * If an item has moved off the current page due to a split, we'll fail to
   * find it and do nothing (this is not an error case --- we assume the item
!  * will eventually get marked in a future indexscan).  Note that because we
!  * hold pin on the target page continuously from initially reading the items
!  * until applying this function, VACUUM cannot have deleted any items from
!  * the page, and so there is no need to search left from the recorded offset.
!  * (This observation also guarantees that the item is still the right one
!  * to delete, which might otherwise be questionable since heap TIDs can get
!  * recycled.)
   */
  void
! _bt_killitems(IndexScanDesc scan, bool haveLock)
  {
  	BTScanOpaque so = (BTScanOpaque) scan->opaque;
  	Page		page;
--- 1721,1755 ----
   * _bt_killitems - set LP_DEAD state for items an indexscan caller has
   * told us were killed
   *
!  * scan->opaque, referenced locally through so, contains information about the
!  * current page and killed tuples thereon (generally, this should only be
!  * called if so->numKilled > 0).
   *
!  * The caller does not have a lock on the page and may or may not have the
!  * page pinned in a buffer.  Note that read-lock is sufficient for setting
!  * LP_DEAD status (which is only a hint).
   *
   * We match items by heap TID before assuming they are the right ones to
   * delete.  We cope with cases where items have moved right due to insertions.
   * If an item has moved off the current page due to a split, we'll fail to
   * find it and do nothing (this is not an error case --- we assume the item
!  * will eventually get marked in a future indexscan).
!  *
!  * Note that if we hold a pin on the target page continuously from initially
!  * reading the items until applying this function, VACUUM cannot have deleted
!  * any items from the page, and so there is no need to search left from the
!  * recorded offset.  (This observation also guarantees that the item is still
!  * the right one to delete, which might otherwise be questionable since heap
!  * TIDs can get recycled.)  This holds true even if the page has been modified
!  * by inserts and page splits, so there is no need to consult the LSN.
!  *
!  * If the pin was released after reading the page, then we re-read it.  If it
!  * has been modified since we read it (as determined by the LSN), we dare not
!  * flag any entries because it is possible that the old entry was vacuumed
!  * away and the TID was re-used by a completely different heap tuple.
   */
  void
! _bt_killitems(IndexScanDesc scan)
  {
  	BTScanOpaque so = (BTScanOpaque) scan->opaque;
  	Page		page;
***************
*** 1749,1767 **** _bt_killitems(IndexScanDesc scan, bool haveLock)
  	OffsetNumber minoff;
  	OffsetNumber maxoff;
  	int			i;
  	bool		killedsomething = false;
  
! 	Assert(BufferIsValid(so->currPos.buf));
  
! 	if (!haveLock)
  		LockBuffer(so->currPos.buf, BT_READ);
  
! 	page = BufferGetPage(so->currPos.buf);
  	opaque = (BTPageOpaque) PageGetSpecialPointer(page);
  	minoff = P_FIRSTDATAKEY(opaque);
  	maxoff = PageGetMaxOffsetNumber(page);
  
! 	for (i = 0; i < so->numKilled; i++)
  	{
  		int			itemIndex = so->killedItems[i];
  		BTScanPosItem *kitem = &so->currPos.items[itemIndex];
--- 1757,1807 ----
  	OffsetNumber minoff;
  	OffsetNumber maxoff;
  	int			i;
+ 	int			numKilled = so->numKilled;
  	bool		killedsomething = false;
  
! 	Assert(BTScanPosIsValid(so->currPos));
  
! 	/*
! 	 * Always reset the scan state, so we don't look for same items on other
! 	 * pages.
! 	 */
! 	so->numKilled = 0;
! 
! 	if (BTScanPosIsPinned(so->currPos))
! 	{
! 		/* The buffer is still pinned, but not locked.  Lock it now. */
  		LockBuffer(so->currPos.buf, BT_READ);
  
! 		page = BufferGetPage(so->currPos.buf);
! 	}
! 	else
! 	{
! 		Buffer		buf;
! 
! 		/* Attempt to re-read the buffer, getting pin and lock. */
! 		buf = _bt_getbuf(scan->indexRelation, so->currPos.currPage, BT_READ);
! 
! 		/* It might not exist anymore; in which case we can't hint it. */
! 		if (!BufferIsValid(buf))
! 			return;
! 
! 		page = BufferGetPage(buf);
! 		if (PageGetLSN(page) == so->currPos.lsn)
! 			so->currPos.buf = buf;
! 		else
! 		{
! 			/* Modified while not pinned means hinting is not safe. */
! 			_bt_relbuf(scan->indexRelation, buf);
! 			return;
! 		}
! 	}
! 
  	opaque = (BTPageOpaque) PageGetSpecialPointer(page);
  	minoff = P_FIRSTDATAKEY(opaque);
  	maxoff = PageGetMaxOffsetNumber(page);
  
! 	for (i = 0; i < numKilled; i++)
  	{
  		int			itemIndex = so->killedItems[i];
  		BTScanPosItem *kitem = &so->currPos.items[itemIndex];
***************
*** 1799,1812 **** _bt_killitems(IndexScanDesc scan, bool haveLock)
  		MarkBufferDirtyHint(so->currPos.buf, true);
  	}
  
! 	if (!haveLock)
! 		LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
! 
! 	/*
! 	 * Always reset the scan state, so we don't look for same items on other
! 	 * pages.
! 	 */
! 	so->numKilled = 0;
  }
  
  
--- 1839,1845 ----
  		MarkBufferDirtyHint(so->currPos.buf, true);
  	}
  
! 	LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK);
  }
  
  
*** a/src/backend/access/nbtree/nbtxlog.c
--- b/src/backend/access/nbtree/nbtxlog.c
***************
*** 395,401 **** btree_xlog_vacuum(XLogReaderState *record)
  	 * unpinned between the lastBlockVacuumed and the current block, if there
  	 * are any.  This prevents replay of the VACUUM from reaching the stage of
  	 * removing heap tuples while there could still be indexscans "in flight"
! 	 * to those particular tuples (see nbtree/README).
  	 *
  	 * It might be worth checking if there are actually any backends running;
  	 * if not, we could just skip this.
--- 395,402 ----
  	 * unpinned between the lastBlockVacuumed and the current block, if there
  	 * are any.  This prevents replay of the VACUUM from reaching the stage of
  	 * removing heap tuples while there could still be indexscans "in flight"
! 	 * to those particular tuples for those scans which could be confused by
! 	 * finding new tuples at the old TID locations (see nbtree/README).
  	 *
  	 * It might be worth checking if there are actually any backends running;
  	 * if not, we could just skip this.
*** a/src/backend/storage/buffer/README
--- b/src/backend/storage/buffer/README
***************
*** 18,25 **** backend to pin a page more than once concurrently; the buffer manager
  handles this efficiently.  It is considered OK to hold a pin for long
  intervals --- for example, sequential scans hold a pin on the current page
  until done processing all the tuples on the page, which could be quite a
! while if the scan is the outer scan of a join.  Similarly, btree index
! scans hold a pin on the current index page.  This is OK because normal
  operations never wait for a page's pin count to drop to zero.  (Anything
  that might need to do such a wait is instead handled by waiting to obtain
  the relation-level lock, which is why you'd better hold one first.)  Pins
--- 18,25 ----
  handles this efficiently.  It is considered OK to hold a pin for long
  intervals --- for example, sequential scans hold a pin on the current page
  until done processing all the tuples on the page, which could be quite a
! while if the scan is the outer scan of a join.  Similarly, a btree index
! scan may hold a pin on the current index page.  This is OK because normal
  operations never wait for a page's pin count to drop to zero.  (Anything
  that might need to do such a wait is instead handled by waiting to obtain
  the relation-level lock, which is why you'd better hold one first.)  Pins
*** a/src/include/access/nbtree.h
--- b/src/include/access/nbtree.h
***************
*** 518,523 **** typedef struct BTScanPosData
--- 518,525 ----
  {
  	Buffer		buf;			/* if valid, the buffer is pinned */
  
+ 	XLogRecPtr	lsn;			/* pos in the WAL stream when page was read */
+ 	BlockNumber currPage;		/* page we've referencd by items array */
  	BlockNumber nextPage;		/* page's right link when we scanned it */
  
  	/*
***************
*** 551,557 **** typedef struct BTScanPosData
  
  typedef BTScanPosData *BTScanPos;
  
! #define BTScanPosIsValid(scanpos) BufferIsValid((scanpos).buf)
  
  /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
  typedef struct BTArrayKeyInfo
--- 553,589 ----
  
  typedef BTScanPosData *BTScanPos;
  
! #define BTScanPosIsPinned(scanpos) \
! ( \
! 	AssertMacro(BlockNumberIsValid((scanpos).currPage) || \
! 				!BufferIsValid((scanpos).buf)), \
! 	BufferIsValid((scanpos).buf) \
! )
! #define BTScanPosUnpin(scanpos) \
! 	do { \
! 		ReleaseBuffer((scanpos).buf); \
! 		(scanpos).buf = InvalidBuffer; \
! 	} while (0)
! #define BTScanPosUnpinIfPinned(scanpos) \
! 	do { \
! 		if (BTScanPosIsPinned(scanpos)) \
! 			BTScanPosUnpin(scanpos); \
! 	} while (0)
! 
! #define BTScanPosIsValid(scanpos) \
! ( \
! 	AssertMacro(BlockNumberIsValid((scanpos).currPage) || \
! 				!BufferIsValid((scanpos).buf)), \
! 	BlockNumberIsValid((scanpos).currPage) \
! )
! #define BTScanPosInvalidate(scanpos) \
! 	do { \
! 		(scanpos).currPage = InvalidBlockNumber; \
! 		(scanpos).nextPage = InvalidBlockNumber; \
! 		(scanpos).buf = InvalidBuffer; \
! 		(scanpos).lsn = InvalidXLogRecPtr; \
! 		(scanpos).nextTupleOffset = 0; \
! 	} while (0);
  
  /* We need one of these for each equality-type SK_SEARCHARRAY scan key */
  typedef struct BTArrayKeyInfo
***************
*** 697,703 **** extern void _bt_preprocess_keys(IndexScanDesc scan);
  extern IndexTuple _bt_checkkeys(IndexScanDesc scan,
  			  Page page, OffsetNumber offnum,
  			  ScanDirection dir, bool *continuescan);
! extern void _bt_killitems(IndexScanDesc scan, bool haveLock);
  extern BTCycleId _bt_vacuum_cycleid(Relation rel);
  extern BTCycleId _bt_start_vacuum(Relation rel);
  extern void _bt_end_vacuum(Relation rel);
--- 729,735 ----
  extern IndexTuple _bt_checkkeys(IndexScanDesc scan,
  			  Page page, OffsetNumber offnum,
  			  ScanDirection dir, bool *continuescan);
! extern void _bt_killitems(IndexScanDesc scan);
  extern BTCycleId _bt_vacuum_cycleid(Relation rel);
  extern BTCycleId _bt_start_vacuum(Relation rel);
  extern void _bt_end_vacuum(Relation rel);
