commit 70973802b461b3752c520a9e43d2bb96ebb36f14
Author: Simon Riggs <simon.riggs@enterprisedb.com>
Date:   Mon Sep 5 14:37:11 2022 +0100

    Improve effectiveness of heap pruning for never visible tuples

diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 9f43bbe25f..15548896b8 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -597,7 +597,8 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 	HeapTupleHeader htup;
 	OffsetNumber latestdead = InvalidOffsetNumber,
 				maxoff = PageGetMaxOffsetNumber(dp),
-				offnum;
+				offnum,
+				priorOffnum;
 	OffsetNumber chainitems[MaxHeapTuplesPerPage];
 	int			nchain = 0,
 				i;
@@ -654,7 +655,8 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 	{
 		ItemId		lp;
 		bool		tupdead,
-					recent_dead;
+					recent_dead,
+					never_visible;
 
 		/* Sanity check (pure paranoia) */
 		if (offnum < FirstOffsetNumber)
@@ -718,7 +720,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 		/*
 		 * Check tuple's visibility status.
 		 */
-		tupdead = recent_dead = false;
+		tupdead = recent_dead = never_visible = false;
 
 		switch ((HTSV_Result) prstate->htsv[offnum])
 		{
@@ -727,16 +729,32 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 				break;
 
 			case HEAPTUPLE_RECENTLY_DEAD:
+			{
+				TransactionId prune_xmin = HeapTupleHeaderGetXmin(htup);
+				TransactionId prune_xmax = HeapTupleHeaderGetUpdateXid(htup);
 				recent_dead = true;
 
+				/*
+				 * Row versions that are both inserted and updated by the same
+				 * transaction will never be visible outside of the transaction
+				 * and might be removable, even though only recently dead.
+				 * This check can be performed very cheaply with data at hand.
+				 *
+				 * It would also be correct to check for rows inserted in one
+				 * subtransaction, then later inserted in a later subtransaction
+				 * within one top-level transaction, but we'd need to hit subtrans
+				 * with lots of requests, so it will be very slow.
+				 */
+				if (TransactionIdEquals(prune_xmin, prune_xmax))
+					never_visible = true;
+
 				/*
 				 * This tuple may soon become DEAD.  Update the hint field so
 				 * that the page is reconsidered for pruning in future.
 				 */
-				heap_prune_record_prunable(prstate,
-										   HeapTupleHeaderGetUpdateXid(htup));
+				heap_prune_record_prunable(prstate,prune_xmax);
 				break;
-
+			}
 			case HEAPTUPLE_DELETE_IN_PROGRESS:
 
 				/*
@@ -769,8 +787,19 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 		 * but we can't advance past anything else.  We have to make sure that
 		 * we don't miss any DEAD tuples, since DEAD tuples that still have
 		 * tuple storage after pruning will confuse VACUUM.
+		 *
+		 * There are some useful edge cases where we can remove tuples, even
+		 * when they are only recently dead:
+		 * - We can remove never_visible tuples at the start of the chain.
+		 * - If the last member of the chain was latestdead then we can also
+		 * remove never_visible tuples immediately afterwards, since doing
+		 * so will not break the chain. Doing this can greatly increase the
+		 * effectiveness of pruning when an application performs an INSERT
+		 * then an UPDATEs the row in the same transaction, or when an app
+		 * performs multiple UPDATEs of the same row in the same transaction.
 		 */
-		if (tupdead)
+		if (tupdead ||
+			(never_visible && latestdead == priorOffnum))
 		{
 			latestdead = offnum;
 			HeapTupleHeaderAdvanceLatestRemovedXid(htup,
@@ -794,6 +823,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
 		 */
 		Assert(ItemPointerGetBlockNumber(&htup->t_ctid) ==
 			   BufferGetBlockNumber(buffer));
+		priorOffnum = offnum;
 		offnum = ItemPointerGetOffsetNumber(&htup->t_ctid);
 		priorXmax = HeapTupleHeaderGetUpdateXid(htup);
 	}
diff --git a/src/test/isolation/expected/never-visible.out b/src/test/isolation/expected/never-visible.out
new file mode 100644
index 0000000000..cbc1f25cb2
--- /dev/null
+++ b/src/test/isolation/expected/never-visible.out
@@ -0,0 +1,12 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1b s1i s2b s2i s2u s2u s2c s2v s1c
+step s1b: BEGIN;
+step s1i: INSERT INTO t (id) VALUES (1);
+step s2b: BEGIN;
+step s2i: INSERT INTO t (id) VALUES (2);
+step s2u: UPDATE t SET txt = 'a' WHERE id = 2;
+step s2u: UPDATE t SET txt = 'a' WHERE id = 2;
+step s2c: COMMIT;
+step s2v: VACUUM t;
+step s1c: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 529a4cbd4d..49f72a7069 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -16,6 +16,7 @@ test: ri-trigger
 test: partial-index
 test: two-ids
 test: multiple-row-versions
+test: never-visible
 test: index-only-scan
 test: predicate-lock-hot-tuple
 test: update-conflict-out
diff --git a/src/test/isolation/specs/never-visible.spec b/src/test/isolation/specs/never-visible.spec
new file mode 100644
index 0000000000..67d995ee87
--- /dev/null
+++ b/src/test/isolation/specs/never-visible.spec
@@ -0,0 +1,28 @@
+# Never visible test
+#
+# Test pruning of rows that were never visible outside of the
+# originating transaction.
+
+setup
+{
+ CREATE TABLE t (id int NOT NULL, txt text) WITH (fillfactor=50);
+}
+
+teardown
+{
+ DROP TABLE t;
+}
+
+session s1
+step s1b	{ BEGIN; }
+step s1i	{ INSERT INTO t (id) VALUES (1); }
+step s1c	{ COMMIT; }
+
+session s2
+step s2b	{ BEGIN; }
+step s2i	{ INSERT INTO t (id) VALUES (2); }
+step s2u	{ UPDATE t SET txt = 'a' WHERE id = 2; }
+step s2c	{ COMMIT; }
+step s2v	{ VACUUM t; }
+
+permutation s1b s1i s2b s2i s2u s2u s2c s2v s1c
