0007_vacuum_enhancements_v21.patch

application/octet-stream

Filename: 0007_vacuum_enhancements_v21.patch
Type: application/octet-stream
Part: 0
Message: Re: Patch: Write Amplification Reduction Method (WARM)

Patch

Format: unified
Series: patch v21
File+
src/backend/access/common/reloptions.c 22 0
src/backend/catalog/system_views.sql 1 0
src/backend/commands/analyze.c 49 11
src/backend/commands/vacuum.c 2 0
src/backend/commands/vacuumlazy.c 100 32
src/backend/parser/gram.y 19 7
src/backend/postmaster/autovacuum.c 51 7
src/backend/postmaster/pgstat.c 48 2
src/backend/utils/adt/pgstatfuncs.c 15 0
src/backend/utils/init/globals.c 3 0
src/backend/utils/misc/guc.c 30 0
src/include/catalog/pg_proc.h 2 0
src/include/commands/vacuum.h 2 0
src/include/foreign/fdwapi.h 2 1
src/include/miscadmin.h 1 0
src/include/nodes/parsenodes.h 2 1
src/include/parser/kwlist.h 1 0
src/include/pgstat.h 9 2
src/include/postmaster/autovacuum.h 2 0
src/include/utils/guc_tables.h 1 0
src/include/utils/rel.h 2 0
src/test/regress/expected/rules.out 3 0
src/test/regress/expected/warm.out 58 0
src/test/regress/sql/warm.sql 47 0
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 72e1253..b856503 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -338,6 +338,24 @@ static relopt_real realRelOpts[] =
 	},
 	{
 		{
+			"autovacuum_warmcleanup_scale_factor",
+			"Number of WARM chains prior to WARM cleanup as a fraction of reltuples",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		-1, 0.0, 100.0
+	},
+	{
+		{
+			"autovacuum_warmcleanup_index_scale_factor",
+			"Number of WARM pointers in an index prior to WARM cleanup as a fraction of total WARM chains",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		-1, 0.0, 100.0
+	},
+	{
+		{
 			"autovacuum_analyze_scale_factor",
 			"Number of tuple inserts, updates or deletes prior to analyze as a fraction of reltuples",
 			RELOPT_KIND_HEAP,
@@ -1341,6 +1359,10 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
 		{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
 		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
+		{"autovacuum_warmcleanup_scale_factor", RELOPT_TYPE_REAL,
+		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, warmcleanup_scale_factor)},
+		{"autovacuum_warmcleanup_index_scale_factor", RELOPT_TYPE_REAL,
+		offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, warmcleanup_index_scale)},
 		{"user_catalog_table", RELOPT_TYPE_BOOL,
 		offsetof(StdRdOptions, user_catalog_table)},
 		{"parallel_workers", RELOPT_TYPE_INT,
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ca44e03..8d06c93 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -533,6 +533,7 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_tuples_warm_updated(C.oid) AS n_tup_warm_upd,
             pg_stat_get_live_tuples(C.oid) AS n_live_tup,
             pg_stat_get_dead_tuples(C.oid) AS n_dead_tup,
+            pg_stat_get_warm_chains(C.oid) AS n_warm_chains,
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 404acb2..6c4fc4e 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -93,7 +93,8 @@ static VacAttrStats *examine_attribute(Relation onerel, int attnum,
 				  Node *index_expr);
 static int acquire_sample_rows(Relation onerel, int elevel,
 					HeapTuple *rows, int targrows,
-					double *totalrows, double *totaldeadrows);
+					double *totalrows, double *totaldeadrows,
+					double *totalwarmchains);
 static int	compare_rows(const void *a, const void *b);
 static int acquire_inherited_sample_rows(Relation onerel, int elevel,
 							  HeapTuple *rows, int targrows,
@@ -320,7 +321,8 @@ do_analyze_rel(Relation onerel, int options, VacuumParams *params,
 	int			targrows,
 				numrows;
 	double		totalrows,
-				totaldeadrows;
+				totaldeadrows,
+				totalwarmchains;
 	HeapTuple  *rows;
 	PGRUsage	ru0;
 	TimestampTz starttime = 0;
@@ -501,7 +503,8 @@ do_analyze_rel(Relation onerel, int options, VacuumParams *params,
 	else
 		numrows = (*acquirefunc) (onerel, elevel,
 								  rows, targrows,
-								  &totalrows, &totaldeadrows);
+								  &totalrows, &totaldeadrows,
+								  &totalwarmchains);
 
 	/*
 	 * Compute the statistics.  Temporary results during the calculations for
@@ -631,7 +634,7 @@ do_analyze_rel(Relation onerel, int options, VacuumParams *params,
 	 */
 	if (!inh)
 		pgstat_report_analyze(onerel, totalrows, totaldeadrows,
-							  (va_cols == NIL));
+							  totalwarmchains, (va_cols == NIL));
 
 	/* If this isn't part of VACUUM ANALYZE, let index AMs do cleanup */
 	if (!(options & VACOPT_VACUUM))
@@ -991,12 +994,14 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
 static int
 acquire_sample_rows(Relation onerel, int elevel,
 					HeapTuple *rows, int targrows,
-					double *totalrows, double *totaldeadrows)
+					double *totalrows, double *totaldeadrows,
+					double *totalwarmchains)
 {
 	int			numrows = 0;	/* # rows now in reservoir */
 	double		samplerows = 0; /* total # rows collected */
 	double		liverows = 0;	/* # live rows seen */
 	double		deadrows = 0;	/* # dead rows seen */
+	double		warmchains = 0;
 	double		rowstoskip = -1;	/* -1 means not set yet */
 	BlockNumber totalblocks;
 	TransactionId OldestXmin;
@@ -1023,9 +1028,14 @@ acquire_sample_rows(Relation onerel, int elevel,
 		Page		targpage;
 		OffsetNumber targoffset,
 					maxoffset;
+		bool		marked[MaxHeapTuplesPerPage];
+		OffsetNumber root_offsets[MaxHeapTuplesPerPage];
 
 		vacuum_delay_point();
 
+		/* Track which root line pointers are already counted. */
+		memset(marked, 0, sizeof (marked));
+
 		/*
 		 * We must maintain a pin on the target page's buffer to ensure that
 		 * the maxoffset value stays good (else concurrent VACUUM might delete
@@ -1041,6 +1051,9 @@ acquire_sample_rows(Relation onerel, int elevel,
 		targpage = BufferGetPage(targbuffer);
 		maxoffset = PageGetMaxOffsetNumber(targpage);
 
+		/* Get all root line pointers first */
+		heap_get_root_tuples(targpage, root_offsets);
+
 		/* Inner loop over all tuples on the selected page */
 		for (targoffset = FirstOffsetNumber; targoffset <= maxoffset; targoffset++)
 		{
@@ -1069,6 +1082,22 @@ acquire_sample_rows(Relation onerel, int elevel,
 			targtuple.t_data = (HeapTupleHeader) PageGetItem(targpage, itemid);
 			targtuple.t_len = ItemIdGetLength(itemid);
 
+			/*
+			 * If this is a WARM-updated tuple, check if we have already seen
+			 * the root line pointer. If not, count this as a WARM chain. This
+			 * ensures that we count every WARM-chain just once, irrespective
+			 * of how many tuples exist in the chain.
+			 */
+			if (HeapTupleHeaderIsWarmUpdated(targtuple.t_data))
+			{
+				OffsetNumber root_offnum = root_offsets[targoffset];
+				if (!marked[root_offnum])
+				{
+					warmchains += 1;
+					marked[root_offnum] = true;
+				}
+			}
+
 			switch (HeapTupleSatisfiesVacuum(&targtuple,
 											 OldestXmin,
 											 targbuffer))
@@ -1200,18 +1229,24 @@ acquire_sample_rows(Relation onerel, int elevel,
 
 	/*
 	 * Estimate total numbers of rows in relation.  For live rows, use
-	 * vac_estimate_reltuples; for dead rows, we have no source of old
-	 * information, so we have to assume the density is the same in unseen
-	 * pages as in the pages we scanned.
+	 * vac_estimate_reltuples; for dead rows and WARM chains, we have no source
+	 * of old information, so we have to assume the density is the same in
+	 * unseen pages as in the pages we scanned.
 	 */
 	*totalrows = vac_estimate_reltuples(onerel, true,
 										totalblocks,
 										bs.m,
 										liverows);
 	if (bs.m > 0)
+	{
 		*totaldeadrows = floor((deadrows / bs.m) * totalblocks + 0.5);
+		*totalwarmchains = floor((warmchains / bs.m) * totalblocks + 0.5);
+	}
 	else
+	{
 		*totaldeadrows = 0.0;
+		*totalwarmchains = 0.0;
+	}
 
 	/*
 	 * Emit some interesting relation info
@@ -1219,11 +1254,13 @@ acquire_sample_rows(Relation onerel, int elevel,
 	ereport(elevel,
 			(errmsg("\"%s\": scanned %d of %u pages, "
 					"containing %.0f live rows and %.0f dead rows; "
-					"%d rows in sample, %.0f estimated total rows",
+					"%d rows in sample, %.0f estimated total rows; "
+					"%.0f warm chains",
 					RelationGetRelationName(onerel),
 					bs.m, totalblocks,
 					liverows, deadrows,
-					numrows, *totalrows)));
+					numrows, *totalrows,
+					*totalwarmchains)));
 
 	return numrows;
 }
@@ -1428,11 +1465,12 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
 				int			childrows;
 				double		trows,
 							tdrows;
+				double		twarmchains;
 
 				/* Fetch a random sample of the child's rows */
 				childrows = (*acquirefunc) (childrel, elevel,
 											rows + numrows, childtargrows,
-											&trows, &tdrows);
+											&trows, &tdrows, &twarmchains);
 
 				/* We may need to convert from child's rowtype to parent's */
 				if (childrows > 0 &&
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 9fbb0eb..52a7838 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -103,6 +103,7 @@ ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel)
 		params.freeze_table_age = 0;
 		params.multixact_freeze_min_age = 0;
 		params.multixact_freeze_table_age = 0;
+		params.warmcleanup_index_scale = -1;
 	}
 	else
 	{
@@ -110,6 +111,7 @@ ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel)
 		params.freeze_table_age = -1;
 		params.multixact_freeze_min_age = -1;
 		params.multixact_freeze_table_age = -1;
+		params.warmcleanup_index_scale = -1;
 	}
 
 	/* user-invoked vacuum is never "for wraparound" */
diff --git a/src/backend/commands/vacuumlazy.c b/src/backend/commands/vacuumlazy.c
index f52490f..87510ea 100644
--- a/src/backend/commands/vacuumlazy.c
+++ b/src/backend/commands/vacuumlazy.c
@@ -187,11 +187,12 @@ static BufferAccessStrategy vac_strategy;
 /* non-export function prototypes */
 static void lazy_scan_heap(Relation onerel, int options,
 			   LVRelStats *vacrelstats, Relation *Irel, int nindexes,
-			   bool aggressive);
+			   bool aggressive, double warmcleanup_index_scale);
 static void lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats);
 static bool lazy_check_needs_freeze(Buffer buf, bool *hastup);
 static void lazy_vacuum_index(Relation indrel,
 				  bool clear_warm,
+				  double warmcleanup_index_scale,
 				  IndexBulkDeleteResult **stats,
 				  LVRelStats *vacrelstats);
 static void lazy_cleanup_index(Relation indrel,
@@ -207,7 +208,8 @@ 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_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks,
+					   bool dowarmcleanup);
 static void lazy_record_dead_tuple(LVRelStats *vacrelstats,
 					   ItemPointer itemptr);
 static void lazy_record_warm_chain(LVRelStats *vacrelstats,
@@ -283,6 +285,9 @@ lazy_vacuum_rel(Relation onerel, int options, VacuumParams *params,
 						  &OldestXmin, &FreezeLimit, &xidFullScanLimit,
 						  &MultiXactCutoff, &mxactFullScanLimit);
 
+	/* Use default if the caller hasn't specified any value */
+	if (params->warmcleanup_index_scale == -1)
+		params->warmcleanup_index_scale = VacuumWarmCleanupIndexScale;
 	/*
 	 * We request an aggressive scan if the table's frozen Xid is now older
 	 * than or equal to the requested Xid full-table scan limit; or if the
@@ -309,7 +314,8 @@ lazy_vacuum_rel(Relation onerel, int options, VacuumParams *params,
 	vacrelstats->hasindex = (nindexes > 0);
 
 	/* Do the vacuuming */
-	lazy_scan_heap(onerel, options, vacrelstats, Irel, nindexes, aggressive);
+	lazy_scan_heap(onerel, options, vacrelstats, Irel, nindexes, aggressive,
+			params->warmcleanup_index_scale);
 
 	/* Done with indexes */
 	vac_close_indexes(nindexes, Irel, NoLock);
@@ -396,7 +402,8 @@ lazy_vacuum_rel(Relation onerel, int options, VacuumParams *params,
 	pgstat_report_vacuum(RelationGetRelid(onerel),
 						 onerel->rd_rel->relisshared,
 						 new_live_tuples,
-						 vacrelstats->new_dead_tuples);
+						 vacrelstats->new_dead_tuples,
+						 vacrelstats->num_non_convertible_warm_chains);
 	pgstat_progress_end_command();
 
 	/* and log the action if appropriate */
@@ -507,10 +514,19 @@ vacuum_log_cleanup_info(Relation rel, LVRelStats *vacrelstats)
  *		If there are no indexes then we can reclaim line pointers on the fly;
  *		dead line pointers need only be retained until all index pointers that
  *		reference them have been killed.
+ *
+ *		warmcleanup_index_scale specifies the number of WARM pointers in an
+ *		index as a fraction of total candidate WARM chains. If we find less
+ *		WARM pointers in an index than the specified fraction, then we don't
+ *		invoke cleanup that index. If WARM cleanup is skipped for any one
+ *		index, the WARM chain can't be cleared in the heap and no further WARM
+ *		updates are possible to such chains. Such chains are also not
+ *		considered for WARM cleanup in other indexes.
  */
 static void
 lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
-			   Relation *Irel, int nindexes, bool aggressive)
+			   Relation *Irel, int nindexes, bool aggressive,
+			   double warmcleanup_index_scale)
 {
 	BlockNumber nblocks,
 				blkno;
@@ -536,6 +552,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		PROGRESS_VACUUM_MAX_DEAD_TUPLES
 	};
 	int64		initprog_val[3];
+	bool		dowarmcleanup = ((options & VACOPT_WARM_CLEANUP) != 0);
 
 	pg_rusage_init(&ru0);
 
@@ -558,7 +575,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 	vacrelstats->nonempty_pages = 0;
 	vacrelstats->latestRemovedXid = InvalidTransactionId;
 
-	lazy_space_alloc(vacrelstats, nblocks);
+	lazy_space_alloc(vacrelstats, nblocks, dowarmcleanup);
 	frozen = palloc(sizeof(xl_heap_freeze_tuple) * MaxHeapTuplesPerPage);
 
 	/* Report that we're scanning the heap, advertising total # of blocks */
@@ -776,7 +793,8 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 			/* Remove index entries */
 			for (i = 0; i < nindexes; i++)
 				lazy_vacuum_index(Irel[i],
-								  (vacrelstats->num_warm_chains > 0),
+								  dowarmcleanup && (vacrelstats->num_warm_chains > 0),
+								  warmcleanup_index_scale,
 								  &indstats[i],
 								  vacrelstats);
 
@@ -1408,7 +1426,8 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats,
 		/* Remove index entries */
 		for (i = 0; i < nindexes; i++)
 			lazy_vacuum_index(Irel[i],
-							  (vacrelstats->num_warm_chains > 0),
+							  dowarmcleanup && (vacrelstats->num_warm_chains > 0),
+							  warmcleanup_index_scale,
 							  &indstats[i],
 							  vacrelstats);
 
@@ -1863,6 +1882,7 @@ lazy_reset_warm_pointer_count(LVRelStats *vacrelstats)
 static void
 lazy_vacuum_index(Relation indrel,
 				  bool clear_warm,
+				  double warmcleanup_index_scale,
 				  IndexBulkDeleteResult **stats,
 				  LVRelStats *vacrelstats)
 {
@@ -1927,25 +1947,55 @@ lazy_vacuum_index(Relation indrel,
 						(*stats)->warm_pointers_removed,
 						(*stats)->clear_pointers_removed)));
 
-		(*stats)->num_warm_pointers = 0;
-		(*stats)->num_clear_pointers = 0;
-		(*stats)->warm_pointers_removed = 0;
-		(*stats)->clear_pointers_removed = 0;
-		(*stats)->pointers_cleared = 0;
+		/*
+		 * If the number of WARM pointers found in the index are more than the
+		 * configured fraction of total candidate WARM chains, then do the
+		 * second index scan to clean up WARM chains.
+		 *
+		 * Otherwise we must set these WARM chains as non-convertible chains.
+		 */
+		if ((*stats)->num_warm_pointers >
+				((double)vacrelstats->num_warm_chains * warmcleanup_index_scale))
+		{
+			(*stats)->num_warm_pointers = 0;
+			(*stats)->num_clear_pointers = 0;
+			(*stats)->warm_pointers_removed = 0;
+			(*stats)->clear_pointers_removed = 0;
+			(*stats)->pointers_cleared = 0;
+
+			*stats = index_bulk_delete(&ivinfo, *stats,
+					lazy_indexvac_phase2, (void *) vacrelstats);
+			ereport(elevel,
+					(errmsg("scanned index \"%s\" to convert WARM pointers, found "
+							"%0.f WARM pointers, %0.f CLEAR pointers, removed "
+							"%0.f WARM pointers, removed %0.f CLEAR pointers, "
+							"cleared %0.f WARM pointers",
+							RelationGetRelationName(indrel),
+							(*stats)->num_warm_pointers,
+							(*stats)->num_clear_pointers,
+							(*stats)->warm_pointers_removed,
+							(*stats)->clear_pointers_removed,
+							(*stats)->pointers_cleared)));
+		}
+		else
+		{
+			int ii;
 
-		*stats = index_bulk_delete(&ivinfo, *stats,
-				lazy_indexvac_phase2, (void *) vacrelstats);
-		ereport(elevel,
-				(errmsg("scanned index \"%s\" to convert WARM pointers, found "
-						"%0.f WARM pointers, %0.f CLEAR pointers, removed "
-						"%0.f WARM pointers, removed %0.f CLEAR pointers, "
-						"cleared %0.f WARM pointers",
-						RelationGetRelationName(indrel),
-						(*stats)->num_warm_pointers,
-						(*stats)->num_clear_pointers,
-						(*stats)->warm_pointers_removed,
-						(*stats)->clear_pointers_removed,
-						(*stats)->pointers_cleared)));
+			/*
+			 * All chains skipped by this index are marked non-convertible.
+			 */
+			for (ii = 0; ii < vacrelstats->num_warm_chains; ii++)
+			{
+				LVWarmChain *chain = &vacrelstats->warm_chains[ii];
+				if (chain->num_warm_pointers > 0 ||
+					chain->num_clear_pointers > 1)
+				{
+					chain->keep_warm_chain = 1;
+					vacrelstats->num_non_convertible_warm_chains++;
+				}
+			}
+
+		}
 	}
 	else
 	{
@@ -2323,7 +2373,8 @@ count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats)
  * See the comments at the head of this file for rationale.
  */
 static void
-lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks)
+lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks,
+				 bool dowarmcleanup)
 {
 	long		maxtuples;
 	int			vac_work_mem = IsAutoVacuumWorkerProcess() &&
@@ -2332,8 +2383,13 @@ lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks)
 
 	if (vacrelstats->hasindex)
 	{
+		/*
+		 * If we're not doing WARM cleanup then the entire memory is available
+		 * for tracking dead tuples. Otherwise it gets split between tracking
+		 * dead tuples and tracking WARM chains.
+		 */
 		maxtuples = (vac_work_mem * 1024L) / (sizeof(ItemPointerData) +
-				sizeof(LVWarmChain));
+				dowarmcleanup ? sizeof(LVWarmChain) : 0);
 		maxtuples = Min(maxtuples, INT_MAX);
 		maxtuples = Min(maxtuples, MaxAllocSize / (sizeof(ItemPointerData) +
 					sizeof(LVWarmChain)));
@@ -2359,11 +2415,18 @@ lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks)
 	 * XXX Cheat for now and allocate the same size array for tracking warm
 	 * chains. maxtuples must have been already adjusted above to ensure we
 	 * don't cross vac_work_mem.
+	 *
+	 * XXX A better strategy seems to consume the available memory from two
+	 * ends and do a round of index cleanup if all available memory is
+	 * exhausted.
 	 */
-	vacrelstats->num_warm_chains = 0;
-	vacrelstats->max_warm_chains = (int) maxtuples;
-	vacrelstats->warm_chains = (LVWarmChain *)
-		palloc0(maxtuples * sizeof(LVWarmChain));
+	if (dowarmcleanup)
+	{
+		vacrelstats->num_warm_chains = 0;
+		vacrelstats->max_warm_chains = (int) maxtuples;
+		vacrelstats->warm_chains = (LVWarmChain *)
+			palloc0(maxtuples * sizeof(LVWarmChain));
+	}
 
 }
 
@@ -2385,6 +2448,8 @@ lazy_record_clear_chain(LVRelStats *vacrelstats,
 		vacrelstats->warm_chains[vacrelstats->num_warm_chains].is_postwarm_chain = 0;
 		vacrelstats->num_warm_chains++;
 	}
+	else
+		vacrelstats->num_non_convertible_warm_chains++;
 }
 
 /*
@@ -2405,6 +2470,8 @@ lazy_record_warm_chain(LVRelStats *vacrelstats,
 		vacrelstats->warm_chains[vacrelstats->num_warm_chains].is_postwarm_chain = 1;
 		vacrelstats->num_warm_chains++;
 	}
+	else
+		vacrelstats->num_non_convertible_warm_chains++;
 }
 
 /*
@@ -2600,6 +2667,7 @@ lazy_indexvac_phase2(ItemPointer itemptr, bool is_warm, void *state)
 		 * index pointers.
 		 */
 		chain->keep_warm_chain = 1;
+		vacrelstats->num_non_convertible_warm_chains++;
 		return IBDCR_KEEP;
 	}
 	return IBDCR_KEEP;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 19dd77d..25b2d69 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -433,7 +433,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	overlay_placing substr_from substr_for
 
 %type <boolean> opt_instead
-%type <boolean> opt_unique opt_concurrently opt_verbose opt_full
+%type <boolean> opt_unique opt_concurrently opt_verbose opt_full opt_warmclean
 %type <boolean> opt_freeze opt_default opt_recheck
 %type <defelt>	opt_binary opt_oids copy_delimiter
 
@@ -684,7 +684,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
 	VERBOSE VERSION_P VIEW VIEWS VOLATILE
 
-	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
+	WARMCLEAN WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
 	XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES
 	XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE
@@ -10058,7 +10058,7 @@ cluster_index_specification:
  *
  *****************************************************************************/
 
-VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
+VacuumStmt: VACUUM opt_full opt_freeze opt_verbose opt_warmclean
 				{
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM;
@@ -10068,11 +10068,13 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_FREEZE;
 					if ($4)
 						n->options |= VACOPT_VERBOSE;
+					if ($5)
+						n->options |= VACOPT_WARM_CLEANUP;
 					n->relation = NULL;
 					n->va_cols = NIL;
 					$$ = (Node *)n;
 				}
-			| VACUUM opt_full opt_freeze opt_verbose qualified_name
+			| VACUUM opt_full opt_freeze opt_verbose opt_warmclean qualified_name
 				{
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM;
@@ -10082,13 +10084,15 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_FREEZE;
 					if ($4)
 						n->options |= VACOPT_VERBOSE;
-					n->relation = $5;
+					if ($5)
+						n->options |= VACOPT_WARM_CLEANUP;
+					n->relation = $6;
 					n->va_cols = NIL;
 					$$ = (Node *)n;
 				}
-			| VACUUM opt_full opt_freeze opt_verbose AnalyzeStmt
+			| VACUUM opt_full opt_freeze opt_verbose opt_warmclean AnalyzeStmt
 				{
-					VacuumStmt *n = (VacuumStmt *) $5;
+					VacuumStmt *n = (VacuumStmt *) $6;
 					n->options |= VACOPT_VACUUM;
 					if ($2)
 						n->options |= VACOPT_FULL;
@@ -10096,6 +10100,8 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_FREEZE;
 					if ($4)
 						n->options |= VACOPT_VERBOSE;
+					if ($5)
+						n->options |= VACOPT_WARM_CLEANUP;
 					$$ = (Node *)n;
 				}
 			| VACUUM '(' vacuum_option_list ')'
@@ -10128,6 +10134,7 @@ vacuum_option_elem:
 			| VERBOSE			{ $$ = VACOPT_VERBOSE; }
 			| FREEZE			{ $$ = VACOPT_FREEZE; }
 			| FULL				{ $$ = VACOPT_FULL; }
+			| WARMCLEAN			{ $$ = VACOPT_WARM_CLEANUP; }
 			| IDENT
 				{
 					if (strcmp($1, "disable_page_skipping") == 0)
@@ -10181,6 +10188,10 @@ opt_freeze: FREEZE									{ $$ = TRUE; }
 			| /*EMPTY*/								{ $$ = FALSE; }
 		;
 
+opt_warmclean: WARMCLEAN							{ $$ = TRUE; }
+			| /*EMPTY*/								{ $$ = FALSE; }
+		;
+
 opt_name_list:
 			'(' name_list ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -14884,6 +14895,7 @@ type_func_name_keyword:
 			| SIMILAR
 			| TABLESAMPLE
 			| VERBOSE
+			| WARMCLEAN
 		;
 
 /* Reserved keyword --- these keywords are usable only as a ColLabel.
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 33ca749..91793e4 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -115,6 +115,8 @@ int			autovacuum_vac_thresh;
 double		autovacuum_vac_scale;
 int			autovacuum_anl_thresh;
 double		autovacuum_anl_scale;
+double		autovacuum_warmcleanup_scale;
+double		autovacuum_warmcleanup_index_scale;
 int			autovacuum_freeze_max_age;
 int			autovacuum_multixact_freeze_max_age;
 
@@ -307,7 +309,8 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 						  Form_pg_class classForm,
 						  PgStat_StatTabEntry *tabentry,
 						  int effective_multixact_freeze_max_age,
-						  bool *dovacuum, bool *doanalyze, bool *wraparound);
+						  bool *dovacuum, bool *doanalyze, bool *wraparound,
+						  bool *dowarmcleanup);
 
 static void autovacuum_do_vac_analyze(autovac_table *tab,
 						  BufferAccessStrategy bstrategy);
@@ -2010,6 +2013,7 @@ do_autovacuum(void)
 		bool		dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
+		bool		dowarmcleanup;
 
 		if (classForm->relkind != RELKIND_RELATION &&
 			classForm->relkind != RELKIND_MATVIEW)
@@ -2049,10 +2053,14 @@ do_autovacuum(void)
 		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
 											 shared, dbentry);
 
-		/* Check if it needs vacuum or analyze */
+		/* 
+		 * Check if it needs vacuum or analyze. For vacuum, also check if it
+		 * needs WARM cleanup.
+		 */
 		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
 								  effective_multixact_freeze_max_age,
-								  &dovacuum, &doanalyze, &wraparound);
+								  &dovacuum, &doanalyze, &wraparound,
+								  &dowarmcleanup);
 
 		/* Relations that need work are added to table_oids */
 		if (dovacuum || doanalyze)
@@ -2105,6 +2113,7 @@ do_autovacuum(void)
 		bool		dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
+		bool		dowarmcleanup;
 
 		/*
 		 * We cannot safely process other backends' temp tables, so skip 'em.
@@ -2135,7 +2144,8 @@ do_autovacuum(void)
 
 		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
 								  effective_multixact_freeze_max_age,
-								  &dovacuum, &doanalyze, &wraparound);
+								  &dovacuum, &doanalyze, &wraparound,
+								  &dowarmcleanup);
 
 		/* ignore analyze for toast tables */
 		if (dovacuum)
@@ -2566,6 +2576,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 	HeapTuple	classTup;
 	bool		dovacuum;
 	bool		doanalyze;
+	bool		dowarmcleanup;
 	autovac_table *tab = NULL;
 	PgStat_StatTabEntry *tabentry;
 	PgStat_StatDBEntry *shared;
@@ -2607,7 +2618,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 
 	relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
 							  effective_multixact_freeze_max_age,
-							  &dovacuum, &doanalyze, &wraparound);
+							  &dovacuum, &doanalyze, &wraparound,
+							  &dowarmcleanup);
 
 	/* ignore ANALYZE for toast tables */
 	if (classForm->relkind == RELKIND_TOASTVALUE)
@@ -2623,6 +2635,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 		int			vac_cost_limit;
 		int			vac_cost_delay;
 		int			log_min_duration;
+		double		warmcleanup_index_scale;
 
 		/*
 		 * Calculate the vacuum cost parameters and the freeze ages.  If there
@@ -2669,19 +2682,26 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 			? avopts->multixact_freeze_table_age
 			: default_multixact_freeze_table_age;
 
+		warmcleanup_index_scale = (avopts &&
+								   avopts->warmcleanup_index_scale >= 0)
+			? avopts->warmcleanup_index_scale
+			: autovacuum_warmcleanup_index_scale;
+
 		tab = palloc(sizeof(autovac_table));
 		tab->at_relid = relid;
 		tab->at_sharedrel = classForm->relisshared;
 		tab->at_vacoptions = VACOPT_SKIPTOAST |
 			(dovacuum ? VACOPT_VACUUM : 0) |
 			(doanalyze ? VACOPT_ANALYZE : 0) |
-			(!wraparound ? VACOPT_NOWAIT : 0);
+			(!wraparound ? VACOPT_NOWAIT : 0) |
+			(dowarmcleanup ? VACOPT_WARM_CLEANUP : 0);
 		tab->at_params.freeze_min_age = freeze_min_age;
 		tab->at_params.freeze_table_age = freeze_table_age;
 		tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age;
 		tab->at_params.multixact_freeze_table_age = multixact_freeze_table_age;
 		tab->at_params.is_wraparound = wraparound;
 		tab->at_params.log_min_duration = log_min_duration;
+		tab->at_params.warmcleanup_index_scale = warmcleanup_index_scale;
 		tab->at_vacuum_cost_limit = vac_cost_limit;
 		tab->at_vacuum_cost_delay = vac_cost_delay;
 		tab->at_relname = NULL;
@@ -2748,7 +2768,8 @@ relation_needs_vacanalyze(Oid relid,
  /* output params below */
 						  bool *dovacuum,
 						  bool *doanalyze,
-						  bool *wraparound)
+						  bool *wraparound,
+						  bool *dowarmcleanup)
 {
 	bool		force_vacuum;
 	bool		av_enabled;
@@ -2760,6 +2781,9 @@ relation_needs_vacanalyze(Oid relid,
 	float4		vac_scale_factor,
 				anl_scale_factor;
 
+	/* constant from reloptions or GUC valriable */
+	float4		warmcleanup_scale_factor;
+
 	/* thresholds calculated from above constants */
 	float4		vacthresh,
 				anlthresh;
@@ -2768,6 +2792,9 @@ relation_needs_vacanalyze(Oid relid,
 	float4		vactuples,
 				anltuples;
 
+	/* number of WARM chains in the table */
+	float4		warmchains;
+
 	/* freeze parameters */
 	int			freeze_max_age;
 	int			multixact_freeze_max_age;
@@ -2800,6 +2827,11 @@ relation_needs_vacanalyze(Oid relid,
 		? relopts->analyze_threshold
 		: autovacuum_anl_thresh;
 
+	/* Use table specific value or the GUC value */
+	warmcleanup_scale_factor = (relopts && relopts->warmcleanup_scale_factor >= 0)
+		? relopts->warmcleanup_scale_factor
+		: autovacuum_warmcleanup_scale;
+
 	freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
 		? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
 		: autovacuum_freeze_max_age;
@@ -2847,6 +2879,7 @@ relation_needs_vacanalyze(Oid relid,
 		reltuples = classForm->reltuples;
 		vactuples = tabentry->n_dead_tuples;
 		anltuples = tabentry->changes_since_analyze;
+		warmchains = tabentry->n_warm_chains;
 
 		vacthresh = (float4) vac_base_thresh + vac_scale_factor * reltuples;
 		anlthresh = (float4) anl_base_thresh + anl_scale_factor * reltuples;
@@ -2863,6 +2896,17 @@ relation_needs_vacanalyze(Oid relid,
 		/* Determine if this table needs vacuum or analyze. */
 		*dovacuum = force_vacuum || (vactuples > vacthresh);
 		*doanalyze = (anltuples > anlthresh);
+
+		/*
+		 * If the number of WARM chains in the is more than the configured
+		 * fraction, then we also do a WARM cleanup. This only triggers at the
+		 * table level, but we then look at each index and do cleanup for the
+		 * index only if the WARM pointers in the index are more than
+		 * configured index-level scale factor. lazy_vacuum_index() later deals
+		 * with that.
+		 */
+		if (*dovacuum && (warmcleanup_scale_factor * reltuples < warmchains))
+			*dowarmcleanup = true;
 	}
 	else
 	{
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index cdfd76e..017ac20 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -199,9 +199,11 @@ typedef struct TwoPhasePgStatRecord
 	PgStat_Counter tuples_inserted;		/* tuples inserted in xact */
 	PgStat_Counter tuples_updated;		/* tuples updated in xact */
 	PgStat_Counter tuples_deleted;		/* tuples deleted in xact */
+	PgStat_Counter tuples_warm_updated;	/* tuples warm updated in xact */
 	PgStat_Counter inserted_pre_trunc;	/* tuples inserted prior to truncate */
 	PgStat_Counter updated_pre_trunc;	/* tuples updated prior to truncate */
 	PgStat_Counter deleted_pre_trunc;	/* tuples deleted prior to truncate */
+	PgStat_Counter warm_updated_pre_trunc;	/* tuples warm updated prior to truncate */
 	Oid			t_id;			/* table's OID */
 	bool		t_shared;		/* is it a shared catalog? */
 	bool		t_truncated;	/* was the relation truncated? */
@@ -1328,7 +1330,8 @@ pgstat_report_autovac(Oid dboid)
  */
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
-					 PgStat_Counter livetuples, PgStat_Counter deadtuples)
+					 PgStat_Counter livetuples, PgStat_Counter deadtuples,
+					 PgStat_Counter warmchains)
 {
 	PgStat_MsgVacuum msg;
 
@@ -1342,6 +1345,7 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	msg.m_vacuumtime = GetCurrentTimestamp();
 	msg.m_live_tuples = livetuples;
 	msg.m_dead_tuples = deadtuples;
+	msg.m_warm_chains = warmchains;
 	pgstat_send(&msg, sizeof(msg));
 }
 
@@ -1357,7 +1361,7 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 void
 pgstat_report_analyze(Relation rel,
 					  PgStat_Counter livetuples, PgStat_Counter deadtuples,
-					  bool resetcounter)
+					  PgStat_Counter warmchains, bool resetcounter)
 {
 	PgStat_MsgAnalyze msg;
 
@@ -1382,12 +1386,14 @@ pgstat_report_analyze(Relation rel,
 		{
 			livetuples -= trans->tuples_inserted - trans->tuples_deleted;
 			deadtuples -= trans->tuples_updated + trans->tuples_deleted;
+			warmchains -= trans->tuples_warm_updated;
 		}
 		/* count stuff inserted by already-aborted subxacts, too */
 		deadtuples -= rel->pgstat_info->t_counts.t_delta_dead_tuples;
 		/* Since ANALYZE's counts are estimates, we could have underflowed */
 		livetuples = Max(livetuples, 0);
 		deadtuples = Max(deadtuples, 0);
+		warmchains = Max(warmchains, 0);
 	}
 
 	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_ANALYZE);
@@ -1398,6 +1404,7 @@ pgstat_report_analyze(Relation rel,
 	msg.m_analyzetime = GetCurrentTimestamp();
 	msg.m_live_tuples = livetuples;
 	msg.m_dead_tuples = deadtuples;
+	msg.m_warm_chains = warmchains;
 	pgstat_send(&msg, sizeof(msg));
 }
 
@@ -1843,7 +1850,10 @@ pgstat_count_heap_update(Relation rel, bool hot, bool warm)
 		if (hot)
 			pgstat_info->t_counts.t_tuples_hot_updated++;
 		else if (warm)
+		{
+			pgstat_info->trans->tuples_warm_updated++;
 			pgstat_info->t_counts.t_tuples_warm_updated++;
+		}
 	}
 }
 
@@ -2006,6 +2016,12 @@ AtEOXact_PgStat(bool isCommit)
 				/* update and delete each create a dead tuple */
 				tabstat->t_counts.t_delta_dead_tuples +=
 					trans->tuples_updated + trans->tuples_deleted;
+				/* 
+				 * commit or abort, a WARM update generates a WARM chain which
+				 * needs cleanup.
+				 */
+				tabstat->t_counts.t_delta_warm_chains +=
+					trans->tuples_warm_updated;
 				/* insert, update, delete each count as one change event */
 				tabstat->t_counts.t_changed_tuples +=
 					trans->tuples_inserted + trans->tuples_updated +
@@ -2016,6 +2032,12 @@ AtEOXact_PgStat(bool isCommit)
 				/* inserted tuples are dead, deleted tuples are unaffected */
 				tabstat->t_counts.t_delta_dead_tuples +=
 					trans->tuples_inserted + trans->tuples_updated;
+				/* 
+				 * commit or abort, a WARM update generates a WARM chain which
+				 * needs cleanup.
+				 */
+				tabstat->t_counts.t_delta_warm_chains +=
+					trans->tuples_warm_updated;
 				/* an aborted xact generates no changed_tuple events */
 			}
 			tabstat->trans = NULL;
@@ -2072,12 +2094,16 @@ AtEOSubXact_PgStat(bool isCommit, int nestDepth)
 						trans->upper->tuples_inserted = trans->tuples_inserted;
 						trans->upper->tuples_updated = trans->tuples_updated;
 						trans->upper->tuples_deleted = trans->tuples_deleted;
+						trans->upper->tuples_warm_updated =
+							trans->tuples_warm_updated;
 					}
 					else
 					{
 						trans->upper->tuples_inserted += trans->tuples_inserted;
 						trans->upper->tuples_updated += trans->tuples_updated;
 						trans->upper->tuples_deleted += trans->tuples_deleted;
+						trans->upper->tuples_warm_updated +=
+							trans->tuples_warm_updated;
 					}
 					tabstat->trans = trans->upper;
 					pfree(trans);
@@ -2113,9 +2139,13 @@ AtEOSubXact_PgStat(bool isCommit, int nestDepth)
 				tabstat->t_counts.t_tuples_inserted += trans->tuples_inserted;
 				tabstat->t_counts.t_tuples_updated += trans->tuples_updated;
 				tabstat->t_counts.t_tuples_deleted += trans->tuples_deleted;
+				tabstat->t_counts.t_tuples_warm_updated +=
+					trans->tuples_warm_updated;
 				/* inserted tuples are dead, deleted tuples are unaffected */
 				tabstat->t_counts.t_delta_dead_tuples +=
 					trans->tuples_inserted + trans->tuples_updated;
+				tabstat->t_counts.t_delta_warm_chains +=
+					trans->tuples_warm_updated;
 				tabstat->trans = trans->upper;
 				pfree(trans);
 			}
@@ -2157,9 +2187,11 @@ AtPrepare_PgStat(void)
 			record.tuples_inserted = trans->tuples_inserted;
 			record.tuples_updated = trans->tuples_updated;
 			record.tuples_deleted = trans->tuples_deleted;
+			record.tuples_warm_updated = trans->tuples_warm_updated;
 			record.inserted_pre_trunc = trans->inserted_pre_trunc;
 			record.updated_pre_trunc = trans->updated_pre_trunc;
 			record.deleted_pre_trunc = trans->deleted_pre_trunc;
+			record.warm_updated_pre_trunc = trans->warm_updated_pre_trunc;
 			record.t_id = tabstat->t_id;
 			record.t_shared = tabstat->t_shared;
 			record.t_truncated = trans->truncated;
@@ -2234,11 +2266,14 @@ pgstat_twophase_postcommit(TransactionId xid, uint16 info,
 		/* forget live/dead stats seen by backend thus far */
 		pgstat_info->t_counts.t_delta_live_tuples = 0;
 		pgstat_info->t_counts.t_delta_dead_tuples = 0;
+		pgstat_info->t_counts.t_delta_warm_chains = 0;
 	}
 	pgstat_info->t_counts.t_delta_live_tuples +=
 		rec->tuples_inserted - rec->tuples_deleted;
 	pgstat_info->t_counts.t_delta_dead_tuples +=
 		rec->tuples_updated + rec->tuples_deleted;
+	pgstat_info->t_counts.t_delta_warm_chains +=
+		rec->tuples_warm_updated;
 	pgstat_info->t_counts.t_changed_tuples +=
 		rec->tuples_inserted + rec->tuples_updated +
 		rec->tuples_deleted;
@@ -2266,12 +2301,16 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted = rec->inserted_pre_trunc;
 		rec->tuples_updated = rec->updated_pre_trunc;
 		rec->tuples_deleted = rec->deleted_pre_trunc;
+		rec->tuples_warm_updated = rec->warm_updated_pre_trunc;
 	}
 	pgstat_info->t_counts.t_tuples_inserted += rec->tuples_inserted;
 	pgstat_info->t_counts.t_tuples_updated += rec->tuples_updated;
 	pgstat_info->t_counts.t_tuples_deleted += rec->tuples_deleted;
+	pgstat_info->t_counts.t_tuples_warm_updated += rec->tuples_warm_updated;
 	pgstat_info->t_counts.t_delta_dead_tuples +=
 		rec->tuples_inserted + rec->tuples_updated;
+	pgstat_info->t_counts.t_delta_warm_chains +=
+		rec->tuples_warm_updated;
 }
 
 
@@ -4335,6 +4374,7 @@ pgstat_get_tab_entry(PgStat_StatDBEntry *dbentry, Oid tableoid, bool create)
 		result->tuples_warm_updated = 0;
 		result->n_live_tuples = 0;
 		result->n_dead_tuples = 0;
+		result->n_warm_chains = 0;
 		result->changes_since_analyze = 0;
 		result->blocks_fetched = 0;
 		result->blocks_hit = 0;
@@ -5445,6 +5485,7 @@ pgstat_recv_tabstat(PgStat_MsgTabstat *msg, int len)
 			tabentry->tuples_warm_updated = tabmsg->t_counts.t_tuples_warm_updated;
 			tabentry->n_live_tuples = tabmsg->t_counts.t_delta_live_tuples;
 			tabentry->n_dead_tuples = tabmsg->t_counts.t_delta_dead_tuples;
+			tabentry->n_warm_chains = tabmsg->t_counts.t_delta_warm_chains;
 			tabentry->changes_since_analyze = tabmsg->t_counts.t_changed_tuples;
 			tabentry->blocks_fetched = tabmsg->t_counts.t_blocks_fetched;
 			tabentry->blocks_hit = tabmsg->t_counts.t_blocks_hit;
@@ -5476,9 +5517,11 @@ pgstat_recv_tabstat(PgStat_MsgTabstat *msg, int len)
 			{
 				tabentry->n_live_tuples = 0;
 				tabentry->n_dead_tuples = 0;
+				tabentry->n_warm_chains = 0;
 			}
 			tabentry->n_live_tuples += tabmsg->t_counts.t_delta_live_tuples;
 			tabentry->n_dead_tuples += tabmsg->t_counts.t_delta_dead_tuples;
+			tabentry->n_warm_chains += tabmsg->t_counts.t_delta_warm_chains;
 			tabentry->changes_since_analyze += tabmsg->t_counts.t_changed_tuples;
 			tabentry->blocks_fetched += tabmsg->t_counts.t_blocks_fetched;
 			tabentry->blocks_hit += tabmsg->t_counts.t_blocks_hit;
@@ -5488,6 +5531,7 @@ pgstat_recv_tabstat(PgStat_MsgTabstat *msg, int len)
 		tabentry->n_live_tuples = Max(tabentry->n_live_tuples, 0);
 		/* Likewise for n_dead_tuples */
 		tabentry->n_dead_tuples = Max(tabentry->n_dead_tuples, 0);
+		tabentry->n_warm_chains = Max(tabentry->n_warm_chains, 0);
 
 		/*
 		 * Add per-table stats to the per-database entry, too.
@@ -5713,6 +5757,7 @@ pgstat_recv_vacuum(PgStat_MsgVacuum *msg, int len)
 
 	tabentry->n_live_tuples = msg->m_live_tuples;
 	tabentry->n_dead_tuples = msg->m_dead_tuples;
+	tabentry->n_warm_chains = msg->m_warm_chains;
 
 	if (msg->m_autovacuum)
 	{
@@ -5747,6 +5792,7 @@ pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len)
 
 	tabentry->n_live_tuples = msg->m_live_tuples;
 	tabentry->n_dead_tuples = msg->m_dead_tuples;
+	tabentry->n_warm_chains = msg->m_warm_chains;
 
 	/*
 	 * If commanded, reset changes_since_analyze to zero.  This forgets any
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b8677f3..814d071 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -191,6 +191,21 @@ pg_stat_get_dead_tuples(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(result);
 }
 
+Datum
+pg_stat_get_warm_chains(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int64		result;
+	PgStat_StatTabEntry *tabentry;
+
+	if ((tabentry = pgstat_fetch_stat_tabentry(relid)) == NULL)
+		result = 0;
+	else
+		result = (int64) (tabentry->n_warm_chains);
+
+	PG_RETURN_INT64(result);
+}
+
 
 Datum
 pg_stat_get_mod_since_analyze(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 08b6030..81fec03 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -130,6 +130,7 @@ int			VacuumCostPageMiss = 10;
 int			VacuumCostPageDirty = 20;
 int			VacuumCostLimit = 200;
 int			VacuumCostDelay = 0;
+double		VacuumWarmCleanupScale;
 
 int			VacuumPageHit = 0;
 int			VacuumPageMiss = 0;
@@ -137,3 +138,5 @@ int			VacuumPageDirty = 0;
 
 int			VacuumCostBalance = 0;		/* working state for vacuum */
 bool		VacuumCostActive = false;
+
+double		VacuumWarmCleanupIndexScale = 1;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 291bf76..b4daa2c 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -3016,6 +3016,36 @@ static struct config_real ConfigureNamesReal[] =
 	},
 
 	{
+		{"autovacuum_warmcleanup_scale_factor", PGC_SIGHUP, AUTOVACUUM,
+			gettext_noop("Number of WARM chains prior to cleanup as a fraction of reltuples."),
+			NULL
+		},
+		&autovacuum_warmcleanup_scale,
+		0.1, 0.0, 100.0,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"autovacuum_warmcleanup_index_scale_factor", PGC_SIGHUP, AUTOVACUUM,
+			gettext_noop("Number of WARM pointers prior to cleanup as a fraction of total WARM chains."),
+			NULL
+		},
+		&autovacuum_warmcleanup_index_scale,
+		0.2, 0.0, 100.0,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"vacuum_warmcleanup_index_scale_factor", PGC_USERSET, WARM_CLEANUP,
+			gettext_noop("Number of WARM pointers in the index prior to cleanup as a fraction of total WARM chains."),
+			NULL
+		},
+		&VacuumWarmCleanupIndexScale,
+		0.2, 0.0, 100.0,
+		NULL, NULL, NULL
+	},
+
+	{
 		{"checkpoint_completion_target", PGC_SIGHUP, WAL_CHECKPOINTS,
 			gettext_noop("Time spent flushing dirty buffers during checkpoint, as fraction of checkpoint interval."),
 			NULL
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 509adda..70025ba 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -2789,6 +2789,8 @@ DATA(insert OID = 2878 (  pg_stat_get_live_tuples	PGNSP PGUID 12 1 0 0 0 f f f f
 DESCR("statistics: number of live tuples");
 DATA(insert OID = 2879 (  pg_stat_get_dead_tuples	PGNSP PGUID 12 1 0 0 0 f f f f t f s r 1 0 20 "26" _null_ _null_ _null_ _null_ _null_ pg_stat_get_dead_tuples _null_ _null_ _null_ ));
 DESCR("statistics: number of dead tuples");
+DATA(insert OID = 3374 (  pg_stat_get_warm_chains	PGNSP PGUID 12 1 0 0 0 f f f f t f s r 1 0 20 "26" _null_ _null_ _null_ _null_ _null_ pg_stat_get_warm_chains _null_ _null_ _null_ ));
+DESCR("statistics: number of warm chains");
 DATA(insert OID = 3177 (  pg_stat_get_mod_since_analyze PGNSP PGUID 12 1 0 0 0 f f f f t f s r 1 0 20 "26" _null_ _null_ _null_ _null_ _null_ pg_stat_get_mod_since_analyze _null_ _null_ _null_ ));
 DESCR("statistics: number of tuples changed since last analyze");
 DATA(insert OID = 1934 (  pg_stat_get_blocks_fetched	PGNSP PGUID 12 1 0 0 0 f f f f t f s r 1 0 20 "26" _null_ _null_ _null_ _null_ _null_ pg_stat_get_blocks_fetched _null_ _null_ _null_ ));
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 541c2fa..9914143 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -145,6 +145,8 @@ typedef struct VacuumParams
 	int			log_min_duration;		/* minimum execution threshold in ms
 										 * at which  verbose logs are
 										 * activated, -1 to use default */
+	double		warmcleanup_index_scale; /* Fraction of WARM pointers to cause
+										  * index WARM cleanup */
 } VacuumParams;
 
 /* GUC parameters */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 6ca44f7..2993b1a 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -134,7 +134,8 @@ typedef void (*ExplainDirectModify_function) (ForeignScanState *node,
 typedef int (*AcquireSampleRowsFunc) (Relation relation, int elevel,
 											   HeapTuple *rows, int targrows,
 												  double *totalrows,
-												  double *totaldeadrows);
+												  double *totaldeadrows,
+												  double *totalwarmchains);
 
 typedef bool (*AnalyzeForeignTable_function) (Relation relation,
 												 AcquireSampleRowsFunc *func,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 4c607b2..901960a 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -255,6 +255,7 @@ extern int	VacuumPageDirty;
 extern int	VacuumCostBalance;
 extern bool VacuumCostActive;
 
+extern double VacuumWarmCleanupIndexScale;
 
 /* in tcop/postgres.c */
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3a71dd5..f842374 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3035,7 +3035,8 @@ typedef enum VacuumOption
 	VACOPT_FULL = 1 << 4,		/* FULL (non-concurrent) vacuum */
 	VACOPT_NOWAIT = 1 << 5,		/* don't wait to get lock (autovacuum only) */
 	VACOPT_SKIPTOAST = 1 << 6,	/* don't process the TOAST table, if any */
-	VACOPT_DISABLE_PAGE_SKIPPING = 1 << 7		/* don't skip any pages */
+	VACOPT_DISABLE_PAGE_SKIPPING = 1 << 7,		/* don't skip any pages */
+	VACOPT_WARM_CLEANUP = 1 << 8	/* do WARM cleanup */
 } VacuumOption;
 
 typedef struct VacuumStmt
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 6cd36c7..632283b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -432,6 +432,7 @@ PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD)
+PG_KEYWORD("warmclean", WARMCLEAN, TYPE_FUNC_NAME_KEYWORD)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD)
 PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 4b7d671..8901c9d 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -110,6 +110,7 @@ typedef struct PgStat_TableCounts
 
 	PgStat_Counter t_delta_live_tuples;
 	PgStat_Counter t_delta_dead_tuples;
+	PgStat_Counter t_delta_warm_chains;
 	PgStat_Counter t_changed_tuples;
 
 	PgStat_Counter t_blocks_fetched;
@@ -167,11 +168,13 @@ typedef struct PgStat_TableXactStatus
 {
 	PgStat_Counter tuples_inserted;		/* tuples inserted in (sub)xact */
 	PgStat_Counter tuples_updated;		/* tuples updated in (sub)xact */
+	PgStat_Counter tuples_warm_updated;	/* tuples warm-updated in (sub)xact */
 	PgStat_Counter tuples_deleted;		/* tuples deleted in (sub)xact */
 	bool		truncated;		/* relation truncated in this (sub)xact */
 	PgStat_Counter inserted_pre_trunc;	/* tuples inserted prior to truncate */
 	PgStat_Counter updated_pre_trunc;	/* tuples updated prior to truncate */
 	PgStat_Counter deleted_pre_trunc;	/* tuples deleted prior to truncate */
+	PgStat_Counter warm_updated_pre_trunc;	/* tuples warm updated prior to truncate */
 	int			nest_level;		/* subtransaction nest level */
 	/* links to other structs for same relation: */
 	struct PgStat_TableXactStatus *upper;		/* next higher subxact if any */
@@ -370,6 +373,7 @@ typedef struct PgStat_MsgVacuum
 	TimestampTz m_vacuumtime;
 	PgStat_Counter m_live_tuples;
 	PgStat_Counter m_dead_tuples;
+	PgStat_Counter m_warm_chains;
 } PgStat_MsgVacuum;
 
 
@@ -388,6 +392,7 @@ typedef struct PgStat_MsgAnalyze
 	TimestampTz m_analyzetime;
 	PgStat_Counter m_live_tuples;
 	PgStat_Counter m_dead_tuples;
+	PgStat_Counter m_warm_chains;
 } PgStat_MsgAnalyze;
 
 
@@ -630,6 +635,7 @@ typedef struct PgStat_StatTabEntry
 
 	PgStat_Counter n_live_tuples;
 	PgStat_Counter n_dead_tuples;
+	PgStat_Counter n_warm_chains;
 	PgStat_Counter changes_since_analyze;
 
 	PgStat_Counter blocks_fetched;
@@ -1131,10 +1137,11 @@ extern void pgstat_reset_single_counter(Oid objectid, PgStat_Single_Reset_Type t
 
 extern void pgstat_report_autovac(Oid dboid);
 extern void pgstat_report_vacuum(Oid tableoid, bool shared,
-					 PgStat_Counter livetuples, PgStat_Counter deadtuples);
+					 PgStat_Counter livetuples, PgStat_Counter deadtuples,
+					 PgStat_Counter warmchains);
 extern void pgstat_report_analyze(Relation rel,
 					  PgStat_Counter livetuples, PgStat_Counter deadtuples,
-					  bool resetcounter);
+					  PgStat_Counter warmchains, bool resetcounter);
 
 extern void pgstat_report_recovery_conflict(int reason);
 extern void pgstat_report_deadlock(void);
diff --git a/src/include/postmaster/autovacuum.h b/src/include/postmaster/autovacuum.h
index 99d7f09..5ac9c8f 100644
--- a/src/include/postmaster/autovacuum.h
+++ b/src/include/postmaster/autovacuum.h
@@ -28,6 +28,8 @@ extern int	autovacuum_freeze_max_age;
 extern int	autovacuum_multixact_freeze_max_age;
 extern int	autovacuum_vac_cost_delay;
 extern int	autovacuum_vac_cost_limit;
+extern double autovacuum_warmcleanup_scale;
+extern double autovacuum_warmcleanup_index_scale;
 
 /* autovacuum launcher PID, only valid when worker is shutting down */
 extern int	AutovacuumLauncherPid;
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index 2da9115..cd4532b 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -68,6 +68,7 @@ enum config_group
 	WAL_SETTINGS,
 	WAL_CHECKPOINTS,
 	WAL_ARCHIVING,
+	WARM_CLEANUP,
 	REPLICATION,
 	REPLICATION_SENDING,
 	REPLICATION_MASTER,
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index cd1976a..9164f60 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -276,6 +276,8 @@ typedef struct AutoVacOpts
 	int			log_min_duration;
 	float8		vacuum_scale_factor;
 	float8		analyze_scale_factor;
+	float8		warmcleanup_scale_factor;
+	float8		warmcleanup_index_scale;
 } AutoVacOpts;
 
 typedef struct StdRdOptions
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a37d443..1b2a69a 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1758,6 +1758,7 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_tuples_warm_updated(c.oid) AS n_tup_warm_upd,
     pg_stat_get_live_tuples(c.oid) AS n_live_tup,
     pg_stat_get_dead_tuples(c.oid) AS n_dead_tup,
+    pg_stat_get_warm_chains(c.oid) AS n_warm_chains,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
@@ -1906,6 +1907,7 @@ pg_stat_sys_tables| SELECT pg_stat_all_tables.relid,
     pg_stat_all_tables.n_tup_warm_upd,
     pg_stat_all_tables.n_live_tup,
     pg_stat_all_tables.n_dead_tup,
+    pg_stat_all_tables.n_warm_chains,
     pg_stat_all_tables.n_mod_since_analyze,
     pg_stat_all_tables.last_vacuum,
     pg_stat_all_tables.last_autovacuum,
@@ -1950,6 +1952,7 @@ pg_stat_user_tables| SELECT pg_stat_all_tables.relid,
     pg_stat_all_tables.n_tup_warm_upd,
     pg_stat_all_tables.n_live_tup,
     pg_stat_all_tables.n_dead_tup,
+    pg_stat_all_tables.n_warm_chains,
     pg_stat_all_tables.n_mod_since_analyze,
     pg_stat_all_tables.last_vacuum,
     pg_stat_all_tables.last_autovacuum,
diff --git a/src/test/regress/expected/warm.out b/src/test/regress/expected/warm.out
index 8aa1505..1346bb1 100644
--- a/src/test/regress/expected/warm.out
+++ b/src/test/regress/expected/warm.out
@@ -745,3 +745,61 @@ SELECT a, b FROM test_toast_warm WHERE b = 104.20;
 (1 row)
 
 DROP TABLE test_toast_warm;
+-- Test VACUUM
+CREATE TABLE test_vacuum_warm (a int unique, b text, c int, d int);
+CREATE INDEX test_vacuum_warm_index1 ON test_vacuum_warm(b);
+CREATE INDEX test_vacuum_warm_index2 ON test_vacuum_warm(c);
+INSERT INTO test_vacuum_warm VALUES (1, 'a', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (2, 'b', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (3, 'c', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (4, 'd', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (5, 'e', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (6, 'f', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (7, 'g', 100, 200);
+UPDATE test_vacuum_warm SET b = 'u', c = 300 WHERE a = 1;
+UPDATE test_vacuum_warm SET b = 'v', c = 300 WHERE a = 2;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 3;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 4;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 5;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 6;
+-- a plain vacuum cannot clear WARM chains.
+SET enable_seqscan = false;
+SET enable_bitmapscan = false;
+SET seq_page_cost = 10000;
+VACUUM test_vacuum_warm;
+-- We expect non-zero heap-fetches here
+EXPLAIN (analyze, costs off, timing off, summary off) SELECT b FROM test_vacuum_warm WHERE b = 'u';
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
+ Index Only Scan using test_vacuum_warm_index1 on test_vacuum_warm (actual rows=1 loops=1)
+   Index Cond: (b = 'u'::text)
+   Heap Fetches: 1
+(3 rows)
+
+-- Now set vacuum_warmcleanup_index_scale_factor such that only
+-- test_vacuum_warm_index2 can be cleaned up.
+SET vacuum_warmcleanup_index_scale_factor=0.5;
+VACUUM WARMCLEAN test_vacuum_warm;
+-- We expect non-zero heap-fetches here
+EXPLAIN (analyze, costs off, timing off, summary off) SELECT b FROM test_vacuum_warm WHERE b = 'u';
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
+ Index Only Scan using test_vacuum_warm_index1 on test_vacuum_warm (actual rows=1 loops=1)
+   Index Cond: (b = 'u'::text)
+   Heap Fetches: 1
+(3 rows)
+
+-- All WARM chains cleaned up, so index-only scan should be used now without
+-- any heap fetches
+SET vacuum_warmcleanup_index_scale_factor=0;
+VACUUM WARMCLEAN test_vacuum_warm;
+-- We expect zero heap-fetches now
+EXPLAIN (analyze, costs off, timing off, summary off) SELECT b FROM test_vacuum_warm WHERE b = 'u';
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
+ Index Only Scan using test_vacuum_warm_index1 on test_vacuum_warm (actual rows=1 loops=1)
+   Index Cond: (b = 'u'::text)
+   Heap Fetches: 0
+(3 rows)
+
+DROP TABLE test_vacuum_warm;
diff --git a/src/test/regress/sql/warm.sql b/src/test/regress/sql/warm.sql
index ab61cfb..0d751e2 100644
--- a/src/test/regress/sql/warm.sql
+++ b/src/test/regress/sql/warm.sql
@@ -284,3 +284,50 @@ SELECT a, b FROM test_toast_warm WHERE b = 104.201;
 SELECT a, b FROM test_toast_warm WHERE b = 104.20;
 
 DROP TABLE test_toast_warm;
+
+
+-- Test VACUUM
+
+CREATE TABLE test_vacuum_warm (a int unique, b text, c int, d int);
+CREATE INDEX test_vacuum_warm_index1 ON test_vacuum_warm(b);
+CREATE INDEX test_vacuum_warm_index2 ON test_vacuum_warm(c);
+
+INSERT INTO test_vacuum_warm VALUES (1, 'a', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (2, 'b', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (3, 'c', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (4, 'd', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (5, 'e', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (6, 'f', 100, 200);
+INSERT INTO test_vacuum_warm VALUES (7, 'g', 100, 200);
+
+UPDATE test_vacuum_warm SET b = 'u', c = 300 WHERE a = 1;
+UPDATE test_vacuum_warm SET b = 'v', c = 300 WHERE a = 2;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 3;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 4;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 5;
+UPDATE test_vacuum_warm SET c = 300 WHERE a = 6;
+
+-- a plain vacuum cannot clear WARM chains.
+SET enable_seqscan = false;
+SET enable_bitmapscan = false;
+SET seq_page_cost = 10000;
+VACUUM test_vacuum_warm;
+-- We expect non-zero heap-fetches here
+EXPLAIN (analyze, costs off, timing off, summary off) SELECT b FROM test_vacuum_warm WHERE b = 'u';
+
+-- Now set vacuum_warmcleanup_index_scale_factor such that only
+-- test_vacuum_warm_index2 can be cleaned up.
+SET vacuum_warmcleanup_index_scale_factor=0.5;
+VACUUM WARMCLEAN test_vacuum_warm;
+-- We expect non-zero heap-fetches here
+EXPLAIN (analyze, costs off, timing off, summary off) SELECT b FROM test_vacuum_warm WHERE b = 'u';
+
+
+-- All WARM chains cleaned up, so index-only scan should be used now without
+-- any heap fetches
+SET vacuum_warmcleanup_index_scale_factor=0;
+VACUUM WARMCLEAN test_vacuum_warm;
+-- We expect zero heap-fetches now
+EXPLAIN (analyze, costs off, timing off, summary off) SELECT b FROM test_vacuum_warm WHERE b = 'u';
+
+DROP TABLE test_vacuum_warm;