vacuum-tidmap-1.patch

text/x-patch

Filename: vacuum-tidmap-1.patch
Type: text/x-patch
Part: 0
Message: Re: Vacuum: allow usage of more than 1GB of work mem

Patch

Format: unified
File+
src/backend/commands/Makefile 1 1
src/backend/commands/vacuumlazy.c 91 150
src/backend/commands/vacuumtidmap.c 519 0
src/include/commands/vacuum.h 19 0
src/test/regress/expected/vacuum.out 71 1
src/test/regress/sql/vacuum.sql 39 1
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 4a6c99e090..634059b8ff 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -20,6 +20,6 @@ OBJS = amcmds.o aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o \
 	policy.o portalcmds.o prepare.o proclang.o publicationcmds.o \
 	schemacmds.o seclabel.o sequence.o statscmds.o subscriptioncmds.o \
 	tablecmds.o tablespace.o trigger.o tsearchcmds.o typecmds.o user.o \
-	vacuum.o vacuumlazy.o variable.o view.o
+	vacuum.o vacuumlazy.o vacuumtidmap.o variable.o view.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c
index d2a006671a..0cc6b98831 100644
--- a/src/backend/commands/vacuumlazy.c
+++ b/src/backend/commands/vacuumlazy.c
@@ -4,23 +4,21 @@
  *	  Concurrent ("lazy") vacuuming.
  *
  *
- * The major space usage for LAZY VACUUM is storage for the array of dead tuple
- * TIDs.  We want to ensure we can vacuum even the very largest relations with
- * finite memory space usage.  To do that, we set upper bounds on the number of
- * tuples we will keep track of at once.
+ * The major space usage for LAZY VACUUM is storage of dead tuple TIDs.
+ * We want to ensure we can vacuum even the very largest relations with
+ * finite memory space usage.  To do that, we set upper bounds on the amount
+ * of memory used to track the TIDs at any one time.
  *
  * We are willing to use at most maintenance_work_mem (or perhaps
  * autovacuum_work_mem) memory space to keep track of dead tuples.  We
- * initially allocate an array of TIDs of that size, with an upper limit that
- * depends on table size (this limit ensures we don't allocate a huge area
- * uselessly for vacuuming small tables).  If the array threatens to overflow,
- * we suspend the heap scan phase and perform a pass of index cleanup and page
- * compaction, then resume the heap scan with an empty TID array.
+ * use a specialized data structure to hold the TIDs, which keeps track of
+ * the memory used.  If the memory limit is about to be exceeded, we suspend
+ * the heap scan phase and perform a pass of index cleanup and page
+ * compaction, then resume the heap scan with an empty tid map.
  *
  * If we're processing a table with no indexes, we can just vacuum each page
  * as we go; there's no need to save up multiple tuples to minimize the number
- * of index scans performed.  So we don't use maintenance_work_mem memory for
- * the TID array, just enough to hold as many heap tuples as fit on one page.
+ * of index scans performed.
  *
  *
  * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
@@ -94,13 +92,6 @@
 	((BlockNumber) (((uint64) 8 * 1024 * 1024 * 1024) / BLCKSZ))
 
 /*
- * Guesstimation of number of dead tuples per page.  This is used to
- * provide an upper limit to memory allocated when vacuuming small
- * tables.
- */
-#define LAZY_ALLOC_TUPLES		MaxHeapTuplesPerPage
-
-/*
  * Before we consider skipping a page that's marked as clean in
  * visibility map, we must've seen at least this many clean pages.
  */
@@ -130,11 +121,7 @@ typedef struct LVRelStats
 	BlockNumber pages_removed;
 	double		tuples_deleted;
 	BlockNumber nonempty_pages; /* actually, last nonempty page + 1 */
-	/* List of TIDs of tuples we intend to delete */
-	/* NB: this list is ordered by TID address */
-	int			num_dead_tuples;	/* current # of entries */
-	int			max_dead_tuples;	/* # slots allocated in array */
-	ItemPointer dead_tuples;	/* array of ItemPointerData */
+	VacuumTidMap *dead_tuples; /* list of TIDs of tuples we intend to delete */
 	int			num_index_scans;
 	TransactionId latestRemovedXid;
 	bool		lock_waiter_detected;
@@ -163,17 +150,17 @@ static void lazy_vacuum_index(Relation indrel,
 static void lazy_cleanup_index(Relation indrel,
 				   IndexBulkDeleteResult *stats,
 				   LVRelStats *vacrelstats);
-static int lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer,
-				 int tupindex, LVRelStats *vacrelstats, Buffer *vmbuffer);
+static void lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer,
+				 OffsetNumber *dead_offsets, int num_dead_tuples,
+				 LVRelStats *vacrelstats, Buffer *vmbuffer);
 static bool should_attempt_truncation(LVRelStats *vacrelstats);
 static void lazy_truncate_heap(Relation onerel, LVRelStats *vacrelstats);
 static BlockNumber count_nondeletable_pages(Relation onerel,
 						 LVRelStats *vacrelstats);
-static void lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks);
 static void lazy_record_dead_tuple(LVRelStats *vacrelstats,
 					   ItemPointer itemptr);
+static void lazy_clear_dead_tuples(LVRelStats *vacrelstats);
 static bool lazy_tid_reaped(ItemPointer itemptr, void *state);
-static int	vac_cmp_itemptr(const void *left, const void *right);
 static bool heap_page_is_all_visible(Relation rel, Buffer buf,
 						 TransactionId *visibility_cutoff_xid, bool *all_frozen);
 
@@ -492,6 +479,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		PROGRESS_VACUUM_MAX_DEAD_TUPLES
 	};
 	int64		initprog_val[3];
+	int			vac_work_mem;
 
 	pg_rusage_init(&ru0);
 
@@ -521,13 +509,22 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 	vacrelstats->nonempty_pages = 0;
 	vacrelstats->latestRemovedXid = InvalidTransactionId;
 
-	lazy_space_alloc(vacrelstats, nblocks);
+	vac_work_mem = IsAutoVacuumWorkerProcess() &&
+		autovacuum_work_mem != -1 ?
+		autovacuum_work_mem : maintenance_work_mem;
+
+	vacrelstats->dead_tuples = CreateVacuumTidMap(vac_work_mem);
+
 	frozen = palloc(sizeof(xl_heap_freeze_tuple) * MaxHeapTuplesPerPage);
 
 	/* Report that we're scanning the heap, advertising total # of blocks */
 	initprog_val[0] = PROGRESS_VACUUM_PHASE_SCAN_HEAP;
 	initprog_val[1] = nblocks;
-	initprog_val[2] = vacrelstats->max_dead_tuples;
+	/*
+	 * XXX: The max number of tuples is just a rough estimate. It doesn't
+	 * take into account the memory needed by internal nodes in the TID map.
+	 */
+	initprog_val[2] = vac_work_mem / sizeof(ItemPointerData);
 	pgstat_progress_update_multi_param(3, initprog_index, initprog_val);
 
 	/*
@@ -611,7 +608,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 					maxoff;
 		bool		tupgone,
 					hastup;
-		int			prev_dead_count;
+		uint16		prev_dead_count;
 		int			nfrozen;
 		Size		freespace;
 		bool		all_visible_according_to_vm = false;
@@ -705,8 +702,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		 * If we are close to overrunning the available space for dead-tuple
 		 * TIDs, pause and do a cycle of vacuuming before we tackle this page.
 		 */
-		if ((vacrelstats->max_dead_tuples - vacrelstats->num_dead_tuples) < MaxHeapTuplesPerPage &&
-			vacrelstats->num_dead_tuples > 0)
+		if (VacuumTidMapIsFull(vacrelstats->dead_tuples))
 		{
 			const int	hvp_index[] = {
 				PROGRESS_VACUUM_PHASE,
@@ -757,7 +753,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 			 * not to reset latestRemovedXid since we want that value to be
 			 * valid.
 			 */
-			vacrelstats->num_dead_tuples = 0;
+			lazy_clear_dead_tuples(vacrelstats);
 			vacrelstats->num_index_scans++;
 
 			/*
@@ -947,7 +943,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		has_dead_tuples = false;
 		nfrozen = 0;
 		hastup = false;
-		prev_dead_count = vacrelstats->num_dead_tuples;
+		prev_dead_count = VacuumTidMapGetNumTuples(vacrelstats->dead_tuples);
 		maxoff = PageGetMaxOffsetNumber(page);
 
 		/*
@@ -1202,10 +1198,24 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		 * instead of doing a second scan.
 		 */
 		if (nindexes == 0 &&
-			vacrelstats->num_dead_tuples > 0)
+			!VacuumTidMapIsEmpty(vacrelstats->dead_tuples))
 		{
+			BlockNumber tblkno;
+			int			ntuples;
+			OffsetNumber *dead_offsets;
+
 			/* Remove tuples from heap */
-			lazy_vacuum_page(onerel, blkno, buf, 0, vacrelstats, &vmbuffer);
+			VacuumTidMapBeginIterate(vacrelstats->dead_tuples);
+			if (!VacuumTidMapNext(vacrelstats->dead_tuples, &tblkno, &ntuples, &dead_offsets))
+				elog(ERROR, "vacuum tid map was empty");
+			if (tblkno != blkno)
+				elog(ERROR, "vacuum tid map contained wrong block");
+
+			lazy_vacuum_page(onerel, blkno, buf, dead_offsets, ntuples, vacrelstats, &vmbuffer);
+
+			if (VacuumTidMapNext(vacrelstats->dead_tuples, &tblkno, &ntuples, &dead_offsets))
+				elog(ERROR, "unexpected entries in vacuum tid map");
+
 			has_dead_tuples = false;
 
 			/*
@@ -1213,7 +1223,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 			 * not to reset latestRemovedXid since we want that value to be
 			 * valid.
 			 */
-			vacrelstats->num_dead_tuples = 0;
+			lazy_clear_dead_tuples(vacrelstats);
 			vacuumed_pages++;
 
 			/*
@@ -1329,7 +1339,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		 * page, so remember its free space as-is.  (This path will always be
 		 * taken if there are no indexes.)
 		 */
-		if (vacrelstats->num_dead_tuples == prev_dead_count)
+		if (VacuumTidMapGetNumTuples(vacrelstats->dead_tuples) == prev_dead_count)
 			RecordPageWithFreeSpace(onerel, blkno, freespace);
 	}
 
@@ -1363,7 +1373,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 
 	/* If any tuples need to be deleted, perform final vacuum cycle */
 	/* XXX put a threshold on min number of tuples here? */
-	if (vacrelstats->num_dead_tuples > 0)
+	if (!VacuumTidMapIsEmpty(vacrelstats->dead_tuples))
 	{
 		const int	hvp_index[] = {
 			PROGRESS_VACUUM_PHASE,
@@ -1467,25 +1477,32 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 static void
 lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats)
 {
-	int			tupindex;
+	int			tottuples;
 	int			npages;
 	PGRUsage	ru0;
 	Buffer		vmbuffer = InvalidBuffer;
+	int			num_dead_tuples;
+	BlockNumber tblk;
+	OffsetNumber *dead_offsets;
+
+	if (VacuumTidMapIsEmpty(vacrelstats->dead_tuples))
+		return;
 
 	pg_rusage_init(&ru0);
 	npages = 0;
 
-	tupindex = 0;
-	while (tupindex < vacrelstats->num_dead_tuples)
+	tottuples = 0;
+
+	VacuumTidMapBeginIterate(vacrelstats->dead_tuples);
+	while (VacuumTidMapNext(vacrelstats->dead_tuples, &tblk, &num_dead_tuples, &dead_offsets))
 	{
-		BlockNumber tblk;
+		int			tupindex = 0;
 		Buffer		buf;
 		Page		page;
 		Size		freespace;
 
 		vacuum_delay_point();
 
-		tblk = ItemPointerGetBlockNumber(&vacrelstats->dead_tuples[tupindex]);
 		buf = ReadBufferExtended(onerel, MAIN_FORKNUM, tblk, RBM_NORMAL,
 								 vac_strategy);
 		if (!ConditionalLockBufferForCleanup(buf))
@@ -1494,8 +1511,9 @@ lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats)
 			++tupindex;
 			continue;
 		}
-		tupindex = lazy_vacuum_page(onerel, tblk, buf, tupindex, vacrelstats,
-									&vmbuffer);
+		lazy_vacuum_page(onerel, tblk, buf,
+						 dead_offsets, num_dead_tuples,
+						 vacrelstats, &vmbuffer);
 
 		/* Now that we've compacted the page, record its available space */
 		page = BufferGetPage(buf);
@@ -1504,6 +1522,8 @@ lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats)
 		UnlockReleaseBuffer(buf);
 		RecordPageWithFreeSpace(onerel, tblk, freespace);
 		npages++;
+
+		tottuples += num_dead_tuples;
 	}
 
 	if (BufferIsValid(vmbuffer))
@@ -1515,7 +1535,7 @@ lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats)
 	ereport(elevel,
 			(errmsg("\"%s\": removed %d row versions in %d pages",
 					RelationGetRelationName(onerel),
-					tupindex, npages),
+					tottuples, npages),
 			 errdetail_internal("%s", pg_rusage_show(&ru0))));
 }
 
@@ -1525,37 +1545,32 @@ lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats)
  *
  * Caller must hold pin and buffer cleanup lock on the buffer.
  *
- * tupindex is the index in vacrelstats->dead_tuples of the first dead
+ * tupindex is the index in seg->dt_tids of the first dead
  * tuple for this page.  We assume the rest follow sequentially.
  * The return value is the first tupindex after the tuples of this page.
  */
-static int
+static void
 lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer,
-				 int tupindex, LVRelStats *vacrelstats, Buffer *vmbuffer)
+				 OffsetNumber *dead_offsets, int num_dead_tuples,
+				 LVRelStats *vacrelstats, Buffer *vmbuffer)
 {
 	Page		page = BufferGetPage(buffer);
-	OffsetNumber unused[MaxOffsetNumber];
-	int			uncnt = 0;
 	TransactionId visibility_cutoff_xid;
 	bool		all_frozen;
+	int			tupindex;
 
 	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, blkno);
 
 	START_CRIT_SECTION();
 
-	for (; tupindex < vacrelstats->num_dead_tuples; tupindex++)
+	for (tupindex = 0; tupindex < num_dead_tuples; tupindex++)
 	{
-		BlockNumber tblk;
 		OffsetNumber toff;
 		ItemId		itemid;
 
-		tblk = ItemPointerGetBlockNumber(&vacrelstats->dead_tuples[tupindex]);
-		if (tblk != blkno)
-			break;				/* past end of tuples for this block */
-		toff = ItemPointerGetOffsetNumber(&vacrelstats->dead_tuples[tupindex]);
+		toff = dead_offsets[tupindex];
 		itemid = PageGetItemId(page, toff);
 		ItemIdSetUnused(itemid);
-		unused[uncnt++] = toff;
 	}
 
 	PageRepairFragmentation(page);
@@ -1572,7 +1587,7 @@ lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer,
 
 		recptr = log_heap_clean(onerel, buffer,
 								NULL, 0, NULL, 0,
-								unused, uncnt,
+								dead_offsets, num_dead_tuples,
 								vacrelstats->latestRemovedXid);
 		PageSetLSN(page, recptr);
 	}
@@ -1616,8 +1631,6 @@ lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer,
 			visibilitymap_set(onerel, blkno, buffer, InvalidXLogRecPtr,
 							  *vmbuffer, visibility_cutoff_xid, flags);
 	}
-
-	return tupindex;
 }
 
 /*
@@ -1697,14 +1710,18 @@ lazy_vacuum_index(Relation indrel,
 	ivinfo.num_heap_tuples = vacrelstats->old_live_tuples;
 	ivinfo.strategy = vac_strategy;
 
+	/* If uninitialized, we have no tuples to delete from the indexes */
+	if (VacuumTidMapIsEmpty(vacrelstats->dead_tuples))
+		return;
+
 	/* Do bulk deletion */
 	*stats = index_bulk_delete(&ivinfo, *stats,
 							   lazy_tid_reaped, (void *) vacrelstats);
 
 	ereport(elevel,
-			(errmsg("scanned index \"%s\" to remove %d row versions",
+			(errmsg("scanned index \"%s\" to remove " UINT64_FORMAT " row versions",
 					RelationGetRelationName(indrel),
-					vacrelstats->num_dead_tuples),
+					VacuumTidMapGetNumTuples(vacrelstats->dead_tuples)),
 			 errdetail_internal("%s", pg_rusage_show(&ru0))));
 }
 
@@ -2071,113 +2088,37 @@ count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats)
 }
 
 /*
- * lazy_space_alloc - space allocation decisions for lazy vacuum
- *
- * See the comments at the head of this file for rationale.
+ * lazy_record_dead_tuple - remember one deletable tuple
  */
 static void
-lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks)
+lazy_record_dead_tuple(LVRelStats *vacrelstats,
+					   ItemPointer itemptr)
 {
-	long		maxtuples;
-	int			vac_work_mem = IsAutoVacuumWorkerProcess() &&
-	autovacuum_work_mem != -1 ?
-	autovacuum_work_mem : maintenance_work_mem;
-
-	if (vacrelstats->hasindex)
-	{
-		maxtuples = (vac_work_mem * 1024L) / sizeof(ItemPointerData);
-		maxtuples = Min(maxtuples, INT_MAX);
-		maxtuples = Min(maxtuples, MaxAllocSize / sizeof(ItemPointerData));
-
-		/* curious coding here to ensure the multiplication can't overflow */
-		if ((BlockNumber) (maxtuples / LAZY_ALLOC_TUPLES) > relblocks)
-			maxtuples = relblocks * LAZY_ALLOC_TUPLES;
-
-		/* stay sane if small maintenance_work_mem */
-		maxtuples = Max(maxtuples, MaxHeapTuplesPerPage);
-	}
-	else
-	{
-		maxtuples = MaxHeapTuplesPerPage;
-	}
-
-	vacrelstats->num_dead_tuples = 0;
-	vacrelstats->max_dead_tuples = (int) maxtuples;
-	vacrelstats->dead_tuples = (ItemPointer)
-		palloc(maxtuples * sizeof(ItemPointerData));
+	VacuumTidMapRecordTid(vacrelstats->dead_tuples, itemptr);
+	pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES,
+								 VacuumTidMapGetNumTuples(vacrelstats->dead_tuples));
 }
 
 /*
- * lazy_record_dead_tuple - remember one deletable tuple
+ * lazy_clear_dead_tuples - reset the dead tuples map
  */
 static void
-lazy_record_dead_tuple(LVRelStats *vacrelstats,
-					   ItemPointer itemptr)
+lazy_clear_dead_tuples(LVRelStats *vacrelstats)
 {
-	/*
-	 * The array shouldn't overflow under normal behavior, but perhaps it
-	 * could if we are given a really small maintenance_work_mem. In that
-	 * case, just forget the last few tuples (we'll get 'em next time).
-	 */
-	if (vacrelstats->num_dead_tuples < vacrelstats->max_dead_tuples)
-	{
-		vacrelstats->dead_tuples[vacrelstats->num_dead_tuples] = *itemptr;
-		vacrelstats->num_dead_tuples++;
-		pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES,
-									 vacrelstats->num_dead_tuples);
-	}
+	VacuumTidMapReset(vacrelstats->dead_tuples);
 }
 
 /*
  *	lazy_tid_reaped() -- is a particular tid deletable?
  *
  *		This has the right signature to be an IndexBulkDeleteCallback.
- *
- *		Assumes dead_tuples array is in sorted order.
  */
 static bool
 lazy_tid_reaped(ItemPointer itemptr, void *state)
 {
 	LVRelStats *vacrelstats = (LVRelStats *) state;
-	ItemPointer res;
-
-	res = (ItemPointer) bsearch((void *) itemptr,
-								(void *) vacrelstats->dead_tuples,
-								vacrelstats->num_dead_tuples,
-								sizeof(ItemPointerData),
-								vac_cmp_itemptr);
-
-	return (res != NULL);
-}
-
-/*
- * Comparator routines for use with qsort() and bsearch().
- */
-static int
-vac_cmp_itemptr(const void *left, const void *right)
-{
-	BlockNumber lblk,
-				rblk;
-	OffsetNumber loff,
-				roff;
-
-	lblk = ItemPointerGetBlockNumber((ItemPointer) left);
-	rblk = ItemPointerGetBlockNumber((ItemPointer) right);
-
-	if (lblk < rblk)
-		return -1;
-	if (lblk > rblk)
-		return 1;
-
-	loff = ItemPointerGetOffsetNumber((ItemPointer) left);
-	roff = ItemPointerGetOffsetNumber((ItemPointer) right);
-
-	if (loff < roff)
-		return -1;
-	if (loff > roff)
-		return 1;
 
-	return 0;
+	return VacuumTidMapContainsTid(vacrelstats->dead_tuples, itemptr);
 }
 
 /*
diff --git a/src/backend/commands/vacuumtidmap.c b/src/backend/commands/vacuumtidmap.c
new file mode 100644
index 0000000000..bb30a0a205
--- /dev/null
+++ b/src/backend/commands/vacuumtidmap.c
@@ -0,0 +1,519 @@
+/*-------------------------------------------------------------------------
+ *
+ * vacuumtidmap.c
+ *	  Data structure to hold TIDs of dead tuples during vacuum.
+ *
+ * Vacuum Tid Map is used to hold the TIDs of dead tuples during VACUUM.
+ * The data structure is a fairly straightforward B-tree, but tailored for
+ * storing TIDs.
+ *
+ * Operations we need to support:
+ *
+ * - Adding new TIDs. TIDs are only added in increasing TID order. Thanks
+ *   to that, we can fill each internal node fully, and never need to split
+ *   existing nodes.
+ *
+ * - Random lookups by TID. This is done heavily while scanning indexes.
+ *
+ * - Scan all TIDs in order. (In the 2nd phase of vacuum) To make that
+ *   simpler, we store a 'next' pointer on each leaf node.
+ *
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/vacuumtidmap.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "commands/vacuum.h"
+#include "utils/memutils.h"
+
+/*
+ * Node structures, for the in-memory B-tree.
+ *
+ * Each leaf node stores a number of TIDs, in a sorted array. There is also
+ * a pointer to the next leaf node, so that the leaf nodes can be easily
+ * walked from beginning to end.
+ *
+ * An internal node holds a number of downlink pointers, to leaf nodes, or
+ * nodes to internal nodes on lower level. For each downlink, the block number
+ * of the first ItemPointer of the corresponding lower level node is stored.
+ * Storing just the block number, and not the full ItemPointer, saves a little
+ * space, as well as some code in the search code. This relies on the
+ * assumption that MAX_LEAF_NODE_ITEMS > MaxHeapNodesPerTuple; otherwise there
+ * might be two leaf nodes with the same block number, and we would not know
+ * which one to descend down to, when searching.
+
+ * Note that because these form just an in-memory data structure, there is no
+ * need for the nodes to be of the same size.
+ */
+
+#define MAX_LEAF_NODE_ITEMS			(16*1024)
+#define MAX_INTERNAL_NODE_ITEMS		128
+
+/* common structure of both leaf and internal nodes. */
+typedef struct
+{
+	int			level;
+} VacuumTidMapNode;
+
+typedef struct VacuumTidMapLeafNode VacuumTidMapLeafNode;
+
+struct VacuumTidMapLeafNode
+{
+	int			level;		/* always 0 on leaf nodes. */
+	VacuumTidMapLeafNode *next;
+	int			num_items;
+	ItemPointerData itemptrs[MAX_LEAF_NODE_ITEMS];
+};
+
+typedef struct
+{
+	int			level;		/* always >= 1 on internal nodes */
+	int			num_items;
+	BlockNumber blknos[MAX_INTERNAL_NODE_ITEMS];
+	VacuumTidMapNode *downlinks[MAX_INTERNAL_NODE_ITEMS];
+} VacuumTidMapInternalNode;
+
+/* Maximum height of the tree. 10 should be plenty.. */
+#define MAX_LEVELS		10
+
+struct VacuumTidMap
+{
+	int			num_levels;
+	uint64		num_entries;    /* total # of tids in the tree */
+
+	/* Memory context, to hold all extra nodes */
+	MemoryContext context;
+
+	/* Memory tracking */
+	uint64		mem_max;
+	uint64		mem_used;
+
+	/*
+	 * 'root' points to the root of the tree (or the only leaf node, if
+	 * num_levels == 1). 'first_leaf' points to the leftmost leaf page.
+	 */
+	VacuumTidMapNode *root;
+	VacuumTidMapLeafNode *first_leaf;
+
+	/*
+	 * Pointer to the rightmost leaf page, and its parent, grandparent etc.
+	 * all the way up to the root.
+	 */
+	VacuumTidMapLeafNode *last_leaf;
+	VacuumTidMapInternalNode *last_parents[MAX_LEVELS];
+
+	/* Iterator support */
+	OffsetNumber *iter_offsets;
+	VacuumTidMapLeafNode *iter_node;
+	int			iter_itemno;
+};
+
+static inline bool vac_itemptr_binsrch(ItemPointer refvalue, ItemPointer arr, int arr_elems);
+static void update_upper(VacuumTidMap *dt, int level, void *new_node, BlockNumber new_node_blkno);
+
+/*
+ * Create a new, initially empty, tidmap.
+ *
+ * 'vac_work_mem' is the max amount of memory to be used. The tidmap doesn't
+ * actually limit the amount of memory used in any way, but it affects when
+ * VacuumTidMapIsFull() says that the tidmap is full.
+ */
+VacuumTidMap *
+CreateVacuumTidMap(int vac_work_mem)
+{
+	VacuumTidMap *dt;
+
+	/*
+	 * Allocate the tid map struct, and the first leaf node in the current
+	 * memory context. Any additional nodes are allocated in a separate
+	 * context, so that they can be freed easily in VacuumTidMapReset().
+	 */
+	dt = (VacuumTidMap *) palloc(sizeof(VacuumTidMap));
+
+	dt->context = AllocSetContextCreate(CurrentMemoryContext,
+										"Vacuum TID map",
+										ALLOCSET_DEFAULT_SIZES);
+	dt->num_entries = 0;
+
+	dt->first_leaf = (VacuumTidMapLeafNode *)
+		palloc(sizeof(VacuumTidMapLeafNode));
+	dt->first_leaf->level = 0;
+	dt->first_leaf->next = NULL;
+	dt->first_leaf->num_items = 0;
+
+	dt->last_leaf = dt->first_leaf;
+	dt->root = (VacuumTidMapNode *) dt->first_leaf;
+	dt->num_levels = 1;
+
+	dt->mem_max = ((uint64) vac_work_mem) * 1024;
+	dt->mem_used = sizeof(VacuumTidMap) + sizeof(VacuumTidMapLeafNode);
+
+	dt->iter_offsets = NULL;	/* no iteration in progress */
+
+	return dt;
+}
+
+/*
+ * Clear all items from the tid map.
+ */
+void
+VacuumTidMapReset(VacuumTidMap *dt)
+{
+	dt->num_entries = 0;
+
+	dt->first_leaf->next = NULL;
+	dt->first_leaf->num_items = 0;
+	dt->last_leaf = dt->first_leaf;
+	dt->root = (VacuumTidMapNode *) dt->first_leaf;
+	dt->num_levels = 1;
+	MemoryContextReset(dt->context);
+
+	dt->mem_used = sizeof(VacuumTidMap) + sizeof(VacuumTidMapLeafNode);
+	dt->iter_offsets = NULL;
+}
+
+bool
+VacuumTidMapIsEmpty(VacuumTidMap *dt)
+{
+	return (dt->num_entries == 0);
+}
+
+/*
+ * Returns 'true', if inserting another heap page's worth of dead tuples would
+ * overrun the allocated memory.
+ */
+bool
+VacuumTidMapIsFull(VacuumTidMap *dt)
+{
+	/* Can we fit a whole heap page's worth of items on this leaf node? */
+	if (MAX_LEAF_NODE_ITEMS - dt->last_leaf->num_items >= MaxHeapTuplesPerPage)
+		return false;
+
+	/*
+	 * Do we have space to allocate another leaf node?
+	 *
+	 * XXX: This doesn't take into account the possibility that allocating a
+	 * new leaf page also requires allocating new internal pages, possibly all
+	 * the way up to the root. So we might still overshoot if that happens.
+	 */
+	if (dt->mem_max - dt->mem_used > sizeof(VacuumTidMapLeafNode))
+		return false;
+
+	return true;
+}
+
+uint64
+VacuumTidMapGetNumTuples(VacuumTidMap *dt)
+{
+	return dt->num_entries;
+}
+
+/*
+ * Begin in-order scan through all the TIDs.
+ */
+void
+VacuumTidMapBeginIterate(VacuumTidMap *dt)
+{
+	if (dt->iter_offsets)
+		elog(ERROR, "iteration on vacuum tid map is already in progress");
+
+	dt->iter_offsets = MemoryContextAlloc(dt->context,
+										  MaxHeapTuplesPerPage * sizeof(OffsetNumber));
+	dt->iter_node = dt->first_leaf;
+	dt->iter_itemno = 0;
+}
+
+/*
+ * Returns next batch of tuples.
+ *
+ * VacuumTidMapBeginIterate() must be called first. VacuumTidMapNext() returns
+ * TIDs one block number at a time, such that each call returns all the TIDs
+ * with the same block number.
+ *
+ * If there are any more entries, returns true, and 'blkno', 'ntuples' and
+ * 'offsets' are filled with the next batch of TIDs.
+ */
+bool
+VacuumTidMapNext(VacuumTidMap *dt, BlockNumber *blkno, int *ntuples, OffsetNumber **offsets)
+{
+	int			curr_itemno;
+	BlockNumber currblk;
+	VacuumTidMapLeafNode *curr_node;
+	int			num_offsets;
+
+	if (!dt->iter_offsets)
+		elog(ERROR, "vacuum tid map iteration is not in progress");
+
+	if (!dt->iter_node)
+	{
+		/* No more TIDs. End the iterator, and return false */
+		pfree(dt->iter_offsets);
+		dt->iter_offsets = NULL;
+		return false;
+	}
+
+	Assert(dt->iter_node->level == 0);
+	Assert(dt->iter_node->num_items > 0);
+
+	/*
+	 * There are more TIDs to return.
+	 *
+	 * Grab the block number from the next TID, and scan forward, collecting
+	 * the offset numbers of all TIDs on the same block.
+	 */
+	curr_node = dt->iter_node;
+	curr_itemno = dt->iter_itemno;
+
+	currblk = ItemPointerGetBlockNumber(&curr_node->itemptrs[curr_itemno]);
+	dt->iter_offsets[0] = ItemPointerGetOffsetNumber(&curr_node->itemptrs[curr_itemno]);
+	num_offsets = 1;
+	curr_itemno++;
+
+	for (;;)
+	{
+		if (curr_itemno >= curr_node->num_items)
+		{
+			/* We have reached end of this node. Step to the next one. */
+			curr_node = curr_node->next;
+			curr_itemno = 0;
+
+			if (!curr_node)
+			{
+				/* Reached the very end. */
+				break;
+			}
+		}
+
+		if (ItemPointerGetBlockNumber(&curr_node->itemptrs[curr_itemno]) != currblk)
+			break;
+
+		dt->iter_offsets[num_offsets] =
+			ItemPointerGetOffsetNumber(&curr_node->itemptrs[curr_itemno]);
+		num_offsets++;
+		curr_itemno++;
+	}
+
+	/* Remember where we stopped, for the next call */
+	dt->iter_node = curr_node;
+	dt->iter_itemno = curr_itemno;
+
+	*blkno = currblk;
+	*ntuples = num_offsets;
+	*offsets = dt->iter_offsets;
+	return true;
+}
+
+/*
+ * Record a TID in the map.
+ *
+ * Tids must be record in order.
+ */
+void
+VacuumTidMapRecordTid(VacuumTidMap *dt, ItemPointer itemptr)
+{
+	VacuumTidMapLeafNode *last_leaf;
+
+	/* The new TID should be larger than the last one recorded */
+	Assert(ItemPointerIsValid(itemptr));
+	Assert(dt->num_entries == 0 ||
+		   ItemPointerCompare(itemptr, &dt->last_leaf->itemptrs[dt->last_leaf->num_items - 1]) > 0);
+
+	/* Allocate a new leaf node if needed */
+	if (dt->last_leaf->num_items == MAX_LEAF_NODE_ITEMS)
+	{
+		VacuumTidMapLeafNode *new_node;
+
+		dt->mem_used += sizeof(VacuumTidMapLeafNode);
+		new_node = (VacuumTidMapLeafNode *)
+			MemoryContextAlloc(dt->context, sizeof(VacuumTidMapLeafNode));
+		new_node->level = 0;
+		new_node->next = NULL;
+		new_node->num_items = 0;
+
+		dt->last_leaf->next = new_node;
+		dt->last_leaf = new_node;
+
+		update_upper(dt, 1, new_node, ItemPointerGetBlockNumber(itemptr));
+	}
+	last_leaf = dt->last_leaf;
+
+	last_leaf->itemptrs[last_leaf->num_items] = *itemptr;
+	last_leaf->num_items++;
+
+	dt->num_entries++;
+}
+
+/*
+ * Insert the downlink into parent node, after creating a new node.
+ *
+ * Recurses if the parent node is full, too.
+ */
+static void
+update_upper(VacuumTidMap *dt, int level, void *new_node,
+			 BlockNumber new_node_blkno)
+{
+	VacuumTidMapInternalNode *parent;
+
+	/* Append to the parent. */
+	if (level >= dt->num_levels)
+	{
+		/* Create new root node. */
+		VacuumTidMapInternalNode *old_root;
+
+		/* MAX_LEVELS should be more than enough, so this shouldn't happen */
+		if (dt->num_levels == MAX_LEVELS)
+			elog(ERROR, "could not expand vacuum tid map, maximum number of levels reached");
+
+		old_root = (VacuumTidMapInternalNode *) dt->root;
+
+		dt->mem_used += sizeof(VacuumTidMapInternalNode);
+		parent = (VacuumTidMapInternalNode *)
+			MemoryContextAlloc(dt->context, sizeof(VacuumTidMapInternalNode));
+		parent->level = level;
+		parent->blknos[0] = old_root->blknos[0];
+		parent->downlinks[0] = (VacuumTidMapNode *) old_root;
+		parent->num_items = 1;
+
+		dt->last_parents[level] = parent;
+		dt->root = (VacuumTidMapNode *) parent;
+		dt->num_levels++;
+	}
+
+	parent = dt->last_parents[level];
+
+	if (parent->num_items < MAX_INTERNAL_NODE_ITEMS)
+	{
+		parent->blknos[parent->num_items] = new_node_blkno;
+		parent->downlinks[parent->num_items] = new_node;
+		parent->num_items++;
+	}
+	else
+	{
+		/* Doesn't fit on the parent. Allocate new parent. */
+		dt->mem_used += sizeof(VacuumTidMapInternalNode);
+		parent = (VacuumTidMapInternalNode *) MemoryContextAlloc(dt->context,
+																 sizeof(VacuumTidMapInternalNode));
+		parent->level = level;
+		parent->blknos[0] = new_node_blkno;
+		parent->downlinks[0] = new_node;
+		parent->num_items = 1;
+
+		dt->last_parents[level] = parent;
+
+		/* Link this new parent to its parent */
+		update_upper(dt, level + 1, parent, new_node_blkno);
+	}
+}
+
+/*
+ * Does the tid map contain the given TID?
+ */
+bool
+VacuumTidMapContainsTid(VacuumTidMap *dt, ItemPointer itemptr)
+{
+	VacuumTidMapLeafNode *leaf_node;
+	VacuumTidMapInternalNode *node;
+	int			level = dt->num_levels - 1;
+
+	if (dt->num_entries == 0 || !ItemPointerIsValid(itemptr))
+		return false;
+
+	/* Descend to find the right leaf node. */
+	node = (VacuumTidMapInternalNode *) dt->root;
+	while (level > 0)
+	{
+		/* binary search in the internal node. */
+		BlockNumber refblk = ItemPointerGetBlockNumber(itemptr);
+		BlockNumber *blknos = node->blknos;
+		BlockNumber left = 0;
+		BlockNumber right = node->num_items - 1;
+		int			mid;
+
+		Assert(node->level == level);
+
+		while (right > left)
+		{
+			BlockNumber blk;
+
+			mid = left + ((right - left) / 2);
+			blk = blknos[mid];
+
+			if (refblk < blk)
+				right = mid;
+			else if (refblk == blk)
+			{
+				left = mid;
+				break;
+			}
+			else
+				left = mid + 1;
+		}
+
+		node = (VacuumTidMapInternalNode *) node->downlinks[left];
+		level--;
+	}
+
+	leaf_node = (VacuumTidMapLeafNode *) node;
+	Assert(leaf_node->level == 0);
+
+	/* Binary search in the leaf node */
+	return vac_itemptr_binsrch(itemptr, leaf_node->itemptrs, leaf_node->num_items);
+}
+
+
+/*
+ * vac_itemptr_binsrch() -- search a sorted array of item pointers
+ *
+ * Returns true if the item is found.
+ *
+ * All item pointers in the array are assumed to be valid
+ */
+static inline bool
+vac_itemptr_binsrch(ItemPointer refvalue, ItemPointer arr, int arr_elems)
+{
+	BlockNumber refblk,	blk;
+	OffsetNumber refoff, off;
+	ItemPointer value;
+	size_t left, right, mid;
+
+	refblk = ItemPointerGetBlockNumberNoCheck(refvalue);
+	refoff = ItemPointerGetOffsetNumberNoCheck(refvalue);
+
+	left = 0;
+	right = arr_elems - 1;
+	while (right > left)
+	{
+		mid = left + ((right - left) / 2);
+		value = &arr[mid];
+
+		blk = ItemPointerGetBlockNumberNoCheck(value);
+		if (refblk < blk)
+		{
+			right = mid;
+		}
+		else if (refblk == blk)
+		{
+			off = ItemPointerGetOffsetNumberNoCheck(value);
+			if (refoff < off)
+				right = mid;
+			else if (refoff == off)
+				return true;
+			else
+				left = mid + 1;
+		}
+		else
+		{
+			left = mid + 1;
+		}
+	}
+
+	return (left < arr_elems);
+}
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 85d472f0a5..f327f5a7a8 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -155,6 +155,25 @@ extern int	vacuum_multixact_freeze_min_age;
 extern int	vacuum_multixact_freeze_table_age;
 
 
+/*
+ * prototypes for Vacuum Tid Map code, in vacuumtidmap.c
+ */
+typedef struct VacuumTidMap VacuumTidMap;
+
+extern VacuumTidMap *CreateVacuumTidMap(int vac_work_mem);
+extern void VacuumTidMapReset(VacuumTidMap *dt);
+
+extern void VacuumTidMapRecordTid(VacuumTidMap *dt, ItemPointer itemptr);
+
+extern bool VacuumTidMapIsFull(VacuumTidMap *dt);
+extern bool VacuumTidMapIsEmpty(VacuumTidMap *dt);
+extern uint64 VacuumTidMapGetNumTuples(VacuumTidMap *dt);
+
+extern bool VacuumTidMapContainsTid(VacuumTidMap *dt, ItemPointer itemptr);
+extern void VacuumTidMapBeginIterate(VacuumTidMap *dt);
+extern bool VacuumTidMapNext(VacuumTidMap *dt,
+				 BlockNumber *blkno, int *ntuples, OffsetNumber **offsets);
+
 /* in commands/vacuum.c */
 extern void ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel);
 extern void vacuum(int options, List *relations, VacuumParams *params,
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d66e2aa3b7..d60523ba99 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -1,7 +1,17 @@
 --
 -- VACUUM
 --
-CREATE TABLE vactst (i INT);
+CREATE FUNCTION wait_barrier() RETURNS void LANGUAGE plpgsql AS $$
+DECLARE vxids text[];
+BEGIN
+ -- wait for all transactions to commit to make deleted tuples vacuumable
+ SELECT array_agg(DISTINCT virtualtransaction) FROM pg_locks WHERE pid <> pg_backend_pid() INTO vxids;
+ WHILE (SELECT count(virtualtransaction) FROM pg_locks WHERE pid <> pg_backend_pid() AND virtualtransaction = ANY(vxids)) > 0 LOOP
+    PERFORM pg_sleep(0.1);
+ END LOOP;
+END
+$$;
+CREATE TABLE vactst (i INT) WITH (autovacuum_enabled = false);
 INSERT INTO vactst VALUES (1);
 INSERT INTO vactst SELECT * FROM vactst;
 INSERT INTO vactst SELECT * FROM vactst;
@@ -22,6 +32,12 @@ SELECT count(*) FROM vactst;
 (1 row)
 
 DELETE FROM vactst WHERE i != 0;
+SELECT wait_barrier();
+ wait_barrier 
+--------------
+ 
+(1 row)
+
 SELECT * FROM vactst;
  i 
 ---
@@ -49,8 +65,20 @@ SELECT count(*) FROM vactst;
 (1 row)
 
 DELETE FROM vactst WHERE i != 0;
+SELECT wait_barrier();
+ wait_barrier 
+--------------
+ 
+(1 row)
+
 VACUUM (FULL) vactst;
 DELETE FROM vactst;
+SELECT wait_barrier();
+ wait_barrier 
+--------------
+ 
+(1 row)
+
 SELECT * FROM vactst;
  i 
 ---
@@ -119,6 +147,48 @@ ANALYZE (nonexistant-arg) does_not_exist;
 ERROR:  syntax error at or near "nonexistant"
 LINE 1: ANALYZE (nonexistant-arg) does_not_exist;
                  ^
+-- large mwm vacuum runs
+CREATE UNLOGGED TABLE vactst2 (i INT) WITH (autovacuum_enabled = false);
+INSERT INTO vactst2 SELECT * from generate_series(1,300000);
+CREATE INDEX ix_vactst2 ON vactst2 (i);
+DELETE FROM vactst2 WHERE i % 4 != 1;
+SET maintenance_work_mem = 1024;
+SELECT wait_barrier();
+ wait_barrier 
+--------------
+ 
+(1 row)
+
+VACUUM vactst2;
+SET maintenance_work_mem TO DEFAULT;
+DROP INDEX ix_vactst2;
+TRUNCATE TABLE vactst2;
+INSERT INTO vactst2 SELECT * from generate_series(1,40);
+CREATE INDEX ix_vactst2 ON vactst2 (i);
+DELETE FROM vactst2;
+SELECT wait_barrier();
+ wait_barrier 
+--------------
+ 
+(1 row)
+
+VACUUM vactst2;
+EXPLAIN (ANALYZE, BUFFERS, COSTS off, TIMING off, SUMMARY off) SELECT * FROM vactst2;
+                 QUERY PLAN                  
+---------------------------------------------
+ Seq Scan on vactst2 (actual rows=0 loops=1)
+(1 row)
+
+SELECT count(*) FROM vactst2;
+ count 
+-------
+     0
+(1 row)
+
+DROP INDEX ix_vactst2;
+TRUNCATE TABLE vactst2;
 DROP TABLE vaccluster;
+DROP TABLE vactst2;
 DROP TABLE vactst;
 DROP TABLE vacparted;
+DROP FUNCTION wait_barrier();
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 275ce2e270..87ea900686 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -2,7 +2,18 @@
 -- VACUUM
 --
 
-CREATE TABLE vactst (i INT);
+CREATE FUNCTION wait_barrier() RETURNS void LANGUAGE plpgsql AS $$
+DECLARE vxids text[];
+BEGIN
+ -- wait for all transactions to commit to make deleted tuples vacuumable
+ SELECT array_agg(DISTINCT virtualtransaction) FROM pg_locks WHERE pid <> pg_backend_pid() INTO vxids;
+ WHILE (SELECT count(virtualtransaction) FROM pg_locks WHERE pid <> pg_backend_pid() AND virtualtransaction = ANY(vxids)) > 0 LOOP
+    PERFORM pg_sleep(0.1);
+ END LOOP;
+END
+$$;
+
+CREATE TABLE vactst (i INT) WITH (autovacuum_enabled = false);
 INSERT INTO vactst VALUES (1);
 INSERT INTO vactst SELECT * FROM vactst;
 INSERT INTO vactst SELECT * FROM vactst;
@@ -18,6 +29,7 @@ INSERT INTO vactst SELECT * FROM vactst;
 INSERT INTO vactst VALUES (0);
 SELECT count(*) FROM vactst;
 DELETE FROM vactst WHERE i != 0;
+SELECT wait_barrier();
 SELECT * FROM vactst;
 VACUUM FULL vactst;
 UPDATE vactst SET i = i + 1;
@@ -35,8 +47,10 @@ INSERT INTO vactst SELECT * FROM vactst;
 INSERT INTO vactst VALUES (0);
 SELECT count(*) FROM vactst;
 DELETE FROM vactst WHERE i != 0;
+SELECT wait_barrier();
 VACUUM (FULL) vactst;
 DELETE FROM vactst;
+SELECT wait_barrier();
 SELECT * FROM vactst;
 
 VACUUM (FULL, FREEZE) vactst;
@@ -93,6 +107,30 @@ ANALYZE vactst (i), vacparted (does_not_exist);
 ANALYZE (VERBOSE) does_not_exist;
 ANALYZE (nonexistant-arg) does_not_exist;
 
+-- large mwm vacuum runs
+CREATE UNLOGGED TABLE vactst2 (i INT) WITH (autovacuum_enabled = false);
+INSERT INTO vactst2 SELECT * from generate_series(1,300000);
+CREATE INDEX ix_vactst2 ON vactst2 (i);
+DELETE FROM vactst2 WHERE i % 4 != 1;
+SET maintenance_work_mem = 1024;
+SELECT wait_barrier();
+VACUUM vactst2;
+SET maintenance_work_mem TO DEFAULT;
+DROP INDEX ix_vactst2;
+TRUNCATE TABLE vactst2;
+
+INSERT INTO vactst2 SELECT * from generate_series(1,40);
+CREATE INDEX ix_vactst2 ON vactst2 (i);
+DELETE FROM vactst2;
+SELECT wait_barrier();
+VACUUM vactst2;
+EXPLAIN (ANALYZE, BUFFERS, COSTS off, TIMING off, SUMMARY off) SELECT * FROM vactst2;
+SELECT count(*) FROM vactst2;
+DROP INDEX ix_vactst2;
+TRUNCATE TABLE vactst2;
+
 DROP TABLE vaccluster;
+DROP TABLE vactst2;
 DROP TABLE vactst;
 DROP TABLE vacparted;
+DROP FUNCTION wait_barrier();