diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 7480c98a15b4d38d54a6b97e3cc1cba4a935a14d..d766467bdb496115e8d2dfa6cfdcf5d2860aaeeb 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -1,5 +1,5 @@
 <!--
-$PostgreSQL: pgsql/doc/src/sgml/indexam.sgml,v 2.7 2005/11/04 23:14:00 petere Exp $
+$PostgreSQL: pgsql/doc/src/sgml/indexam.sgml,v 2.8 2006/02/11 23:31:32 tgl Exp $
 -->
 
 <chapter id="indexam">
@@ -200,6 +200,12 @@ ambulkdelete (Relation indexRelation,
    struct containing statistics about the effects of the deletion operation.
   </para>
 
+  <para>
+   If <literal>callback_state</> is NULL then no tuples are to be deleted.
+   The index AM may choose to optimize this case (eg by not scanning the
+   index) but it is still expected to deliver accurate statistics.
+  </para>
+
   <para>
 <programlisting>
 IndexBulkDeleteResult *
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index afd743a5a2642c4841300de9084050e612c13634..b96d84fd024ebc00a14ca265b84f80ec821f7320 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -8,7 +8,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/access/gist/gistvacuum.c,v 1.13 2006/02/11 17:14:08 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/access/gist/gistvacuum.c,v 1.14 2006/02/11 23:31:33 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -25,16 +25,19 @@
 #include "storage/freespace.h"
 #include "storage/smgr.h"
 
-/* filled by gistbulkdelete, cleared by gistvacuumpcleanup */
-static bool needFullVacuum = false;
 
+typedef struct GistBulkDeleteResult
+{
+	IndexBulkDeleteResult std;	/* common state */
+	bool		needFullVacuum;
+} GistBulkDeleteResult;
 
 typedef struct
 {
 	GISTSTATE	giststate;
 	Relation	index;
 	MemoryContext opCtx;
-	IndexBulkDeleteResult *result;
+	GistBulkDeleteResult *result;
 } GistVacuum;
 
 typedef struct
@@ -44,6 +47,7 @@ typedef struct
 	bool		emptypage;
 } ArrayTuple;
 
+
 static ArrayTuple
 gistVacuumUpdate(GistVacuum *gv, BlockNumber blkno, bool needunion)
 {
@@ -125,7 +129,7 @@ gistVacuumUpdate(GistVacuum *gv, BlockNumber blkno, bool needunion)
 					if (chldtuple.ituplen > 1)
 					{
 						/*
-						 * child was splitted, so we need mark completion
+						 * child was split, so we need mark completion
 						 * insert(split)
 						 */
 						int			j;
@@ -262,7 +266,7 @@ gistVacuumUpdate(GistVacuum *gv, BlockNumber blkno, bool needunion)
 				needwrite = true;
 				res.emptypage = true;
 				GistPageSetDeleted(page);
-				gv->result->pages_deleted++;
+				gv->result->std.pages_deleted++;
 			}
 		}
 		else
@@ -329,9 +333,9 @@ gistVacuumUpdate(GistVacuum *gv, BlockNumber blkno, bool needunion)
 }
 
 /*
- * For usial vacuum just update FSM, for full vacuum
+ * For usual vacuum just update FSM, for full vacuum
  * reforms parent tuples if some of childs was deleted or changed,
- * update invalid tuples (they can exsist from last crash recovery only),
+ * update invalid tuples (they can exist from last crash recovery only),
  * tries to get smaller index
  */
 
@@ -340,7 +344,7 @@ gistvacuumcleanup(PG_FUNCTION_ARGS)
 {
 	Relation	rel = (Relation) PG_GETARG_POINTER(0);
 	IndexVacuumCleanupInfo *info = (IndexVacuumCleanupInfo *) PG_GETARG_POINTER(1);
-	IndexBulkDeleteResult *stats = (IndexBulkDeleteResult *) PG_GETARG_POINTER(2);
+	GistBulkDeleteResult *stats = (GistBulkDeleteResult *) PG_GETARG_POINTER(2);
 	BlockNumber npages,
 				blkno;
 	BlockNumber nFreePages,
@@ -377,13 +381,11 @@ gistvacuumcleanup(PG_FUNCTION_ARGS)
 		freeGISTstate(&(gv.giststate));
 		MemoryContextDelete(gv.opCtx);
 	}
-	else if (needFullVacuum)
+	else if (stats->needFullVacuum)
 		ereport(NOTICE,
 				(errmsg("index \"%s\" needs VACUUM FULL or REINDEX to finish crash recovery",
 						RelationGetRelationName(rel))));
 
-	needFullVacuum = false;
-
 	if (info->vacuum_full)
 		needLock = false;		/* relation locked with AccessExclusiveLock */
 	else
@@ -438,23 +440,30 @@ gistvacuumcleanup(PG_FUNCTION_ARGS)
 
 		if (lastBlock > lastFilledBlock)
 			RelationTruncate(rel, lastFilledBlock + 1);
-		stats->pages_removed = lastBlock - lastFilledBlock;
+		stats->std.pages_removed = lastBlock - lastFilledBlock;
 	}
 
 	RecordIndexFreeSpace(&rel->rd_node, nFreePages, freePages);
 	pfree(freePages);
 
 	/* return statistics */
-	stats->pages_free = nFreePages;
+	stats->std.pages_free = nFreePages;
 	if (needLock)
 		LockRelationForExtension(rel, ExclusiveLock);
-	stats->num_pages = RelationGetNumberOfBlocks(rel);
+	stats->std.num_pages = RelationGetNumberOfBlocks(rel);
 	if (needLock)
 		UnlockRelationForExtension(rel, ExclusiveLock);
 
 	if (info->vacuum_full)
 		UnlockRelation(rel, AccessExclusiveLock);
 
+	/* if gistbulkdelete skipped the scan, use heap's tuple count */
+	if (stats->std.num_index_tuples < 0)
+	{
+		Assert(info->num_heap_tuples >= 0);
+		stats->std.num_index_tuples = info->num_heap_tuples;
+	}
+
 	PG_RETURN_POINTER(stats);
 }
 
@@ -500,15 +509,33 @@ gistbulkdelete(PG_FUNCTION_ARGS)
 	Relation	rel = (Relation) PG_GETARG_POINTER(0);
 	IndexBulkDeleteCallback callback = (IndexBulkDeleteCallback) PG_GETARG_POINTER(1);
 	void	   *callback_state = (void *) PG_GETARG_POINTER(2);
-	IndexBulkDeleteResult *result = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
+	GistBulkDeleteResult *result;
 	GistBDItem *stack,
 			   *ptr;
 	bool		needLock;
 
-	stack = (GistBDItem *) palloc0(sizeof(GistBDItem));
+	result = (GistBulkDeleteResult *) palloc0(sizeof(GistBulkDeleteResult));
 
-	stack->blkno = GIST_ROOT_BLKNO;
-	needFullVacuum = false;
+	/*
+	 * We can skip the scan entirely if there's nothing to delete (indicated
+	 * by callback_state == NULL) and the index isn't partial.  For a partial
+	 * index we must scan in order to derive a trustworthy tuple count.
+	 *
+	 * XXX as of PG 8.2 this is dead code because GIST indexes are always
+	 * effectively partial ... but keep it anyway in case our null-handling
+	 * gets fixed.
+	 */
+	if (callback_state || vac_is_partial_index(rel))
+	{
+		stack = (GistBDItem *) palloc0(sizeof(GistBDItem));
+		stack->blkno = GIST_ROOT_BLKNO;
+	}
+	else
+	{
+		/* skip scan and set flag for gistvacuumcleanup */
+		stack = NULL;
+		result->std.num_index_tuples = -1;
+	}
 
 	while (stack)
 	{
@@ -561,11 +588,11 @@ gistbulkdelete(PG_FUNCTION_ARGS)
 					i--;
 					maxoff--;
 					ntodelete++;
-					result->tuples_removed += 1;
+					result->std.tuples_removed += 1;
 					Assert(maxoff == PageGetMaxOffsetNumber(page));
 				}
 				else
-					result->num_index_tuples += 1;
+					result->std.num_index_tuples += 1;
 			}
 
 			if (ntodelete)
@@ -615,7 +642,7 @@ gistbulkdelete(PG_FUNCTION_ARGS)
 				stack->next = ptr;
 
 				if (GistTupleIsInvalid(idxtuple))
-					needFullVacuum = true;
+					result->needFullVacuum = true;
 			}
 		}
 
@@ -634,7 +661,7 @@ gistbulkdelete(PG_FUNCTION_ARGS)
 
 	if (needLock)
 		LockRelationForExtension(rel, ExclusiveLock);
-	result->num_pages = RelationGetNumberOfBlocks(rel);
+	result->std.num_pages = RelationGetNumberOfBlocks(rel);
 	if (needLock)
 		UnlockRelationForExtension(rel, ExclusiveLock);
 
diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c
index cb82a38d901793542c82ea9379c70ab9b66cf59e..ca0d6ec96dd6a0985f3a029da1f7ea3fce1746e4 100644
--- a/src/backend/access/hash/hash.c
+++ b/src/backend/access/hash/hash.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/access/hash/hash.c,v 1.85 2006/02/11 17:14:08 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/access/hash/hash.c,v 1.86 2006/02/11 23:31:33 tgl Exp $
  *
  * NOTES
  *	  This file contains only the public interface routines.
@@ -517,6 +517,18 @@ hashbulkdelete(PG_FUNCTION_ARGS)
 	cur_maxbucket = orig_maxbucket;
 
 loop_top:
+
+	/*
+	 * If we don't have anything to delete, skip the scan, and report the
+	 * number of tuples shown in the metapage.  (Unlike btree and gist,
+	 * we can trust this number even for a partial index.)
+	 */
+	if (!callback_state)
+	{
+		cur_bucket = cur_maxbucket + 1;
+		num_index_tuples = local_metapage.hashm_ntuples;
+	}
+
 	while (cur_bucket <= cur_maxbucket)
 	{
 		BlockNumber bucket_blkno;
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 6a681192cafbb0cdca6fd18cf8afc8aa2707546e..69c9ecb918987e9b7d8863b5fe2d4385cc8a8aa2 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -8,7 +8,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/access/index/indexam.c,v 1.89 2006/02/11 17:14:08 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/access/index/indexam.c,v 1.90 2006/02/11 23:31:33 tgl Exp $
  *
  * INTERFACE ROUTINES
  *		index_open		- open an index relation by relation OID
@@ -685,6 +685,10 @@ index_getmulti(IndexScanDesc scan,
  *		callback routine tells whether a given main-heap tuple is
  *		to be deleted
  *
+ *		if callback_state is NULL then there are no tuples to be deleted;
+ *		index AM can choose to avoid work in this case, but must still
+ *		follow the protocol of returning statistical info.
+ * 
  *		return value is an optional palloc'd struct of statistics
  * ----------------
  */
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 4fb70302d7aaf67337e5abddc2a7da60e4ea47e2..e28faef141d83b966f3ba34d7dad669e7ffbdb16 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -12,7 +12,7 @@
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/access/nbtree/nbtree.c,v 1.138 2006/02/11 17:14:08 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/access/nbtree/nbtree.c,v 1.139 2006/02/11 23:31:33 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -564,8 +564,22 @@ btbulkdelete(PG_FUNCTION_ARGS)
 	 * further to its right, which the indexscan will have no pin on.)	We can
 	 * skip obtaining exclusive lock on empty pages though, since no indexscan
 	 * could be stopped on those.
+	 *
+	 * We can skip the scan entirely if there's nothing to delete (indicated
+	 * by callback_state == NULL) and the index isn't partial.  For a partial
+	 * index we must scan in order to derive a trustworthy tuple count.
 	 */
-	buf = _bt_get_endpoint(rel, 0, false);
+	if (callback_state || vac_is_partial_index(rel))
+	{
+		buf = _bt_get_endpoint(rel, 0, false);
+	}
+	else
+	{
+		/* skip scan and set flag for btvacuumcleanup */
+		buf = InvalidBuffer;
+		num_index_tuples = -1;
+	}
+
 	if (BufferIsValid(buf))		/* check for empty index */
 	{
 		for (;;)
@@ -836,6 +850,13 @@ btvacuumcleanup(PG_FUNCTION_ARGS)
 	stats->pages_deleted = pages_deleted;
 	stats->pages_free = nFreePages;
 
+	/* if btbulkdelete skipped the scan, use heap's tuple count */
+	if (stats->num_index_tuples < 0)
+	{
+		Assert(info->num_heap_tuples >= 0);
+		stats->num_index_tuples = info->num_heap_tuples;
+	}
+
 	PG_RETURN_POINTER(stats);
 }
 
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index d60c433095d1147142c96dfd42490296de656b17..3153a0a559a928f6fd037bfa6a8bf01fb0a1c78f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -13,7 +13,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/commands/vacuum.c,v 1.323 2006/02/11 17:14:09 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/commands/vacuum.c,v 1.324 2006/02/11 23:31:33 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -2955,6 +2955,7 @@ scan_index(Relation indrel, double num_tuples)
 	/* Do post-VACUUM cleanup, even though we deleted nothing */
 	vcinfo.vacuum_full = true;
 	vcinfo.message_level = elevel;
+	vcinfo.num_heap_tuples = num_tuples;
 
 	stats = index_vacuum_cleanup(indrel, &vcinfo, stats);
 
@@ -3022,6 +3023,7 @@ vacuum_index(VacPageList vacpagelist, Relation indrel,
 	/* Do post-VACUUM cleanup */
 	vcinfo.vacuum_full = true;
 	vcinfo.message_level = elevel;
+	vcinfo.num_heap_tuples = num_tuples + keep_tuples;
 
 	stats = index_vacuum_cleanup(indrel, &vcinfo, stats);
 
diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c
index a65c269fc8c8849b6b76e172f1da0d2075a73abc..2c1ba517638aaf76e1683c6a5b4796372abe0da5 100644
--- a/src/backend/commands/vacuumlazy.c
+++ b/src/backend/commands/vacuumlazy.c
@@ -31,7 +31,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/backend/commands/vacuumlazy.c,v 1.65 2006/02/11 17:14:09 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/backend/commands/vacuumlazy.c,v 1.66 2006/02/11 23:31:34 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -625,6 +625,7 @@ lazy_scan_index(Relation indrel, LVRelStats *vacrelstats)
 	/* Do post-VACUUM cleanup, even though we deleted nothing */
 	vcinfo.vacuum_full = false;
 	vcinfo.message_level = elevel;
+	vcinfo.num_heap_tuples = vacrelstats->rel_tuples;
 
 	stats = index_vacuum_cleanup(indrel, &vcinfo, stats);
 
@@ -697,6 +698,9 @@ lazy_vacuum_index(Relation indrel,
 	/* Do post-VACUUM cleanup */
 	vcinfo.vacuum_full = false;
 	vcinfo.message_level = elevel;
+	/* We don't yet know rel_tuples, so pass -1 */
+	/* index_bulk_delete can't have skipped scan anyway ... */
+	vcinfo.num_heap_tuples = -1;
 
 	stats = index_vacuum_cleanup(indrel, &vcinfo, stats);
 
diff --git a/src/include/access/genam.h b/src/include/access/genam.h
index 859c4e53b2943e108efd3f93a2bd414ace8b0ba6..eace51db0e6f106aa33781bf8ff49b7f8999b98f 100644
--- a/src/include/access/genam.h
+++ b/src/include/access/genam.h
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * $PostgreSQL: pgsql/src/include/access/genam.h,v 1.56 2006/02/11 17:14:09 momjian Exp $
+ * $PostgreSQL: pgsql/src/include/access/genam.h,v 1.57 2006/02/11 23:31:34 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -51,6 +51,7 @@ typedef struct IndexVacuumCleanupInfo
 {
 	bool		vacuum_full;	/* VACUUM FULL (we have exclusive lock) */
 	int			message_level;	/* ereport level for progress messages */
+	double		num_heap_tuples;	/* tuples remaining in heap */
 } IndexVacuumCleanupInfo;
 
 /* Struct for heap-or-index scans of system tables */