From 94397b99da5a1b78b06feaa4dcec3d51e3c9fc8a Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Tue, 10 Mar 2026 08:17:31 -0400
Subject: [PATCH v39 2/8] Identify modified indexed attributes in the executor
 on UPDATE

heap_update() decides which indexed attributes an UPDATE changed by
comparing the old and new tuples inside HeapDetermineColumnsInfo(), with
the buffer locked.  That knowledge is not heap-specific, and the
executor needs it before it calls the table AM to do anything smarter:
an index whose key did not change does not need a new entry, whatever
the table AM does.  Determine it in the executor instead.

ExecUpdateModifiedIdxAttrs() compares the old and new slots over the
relation's indexed attributes and passes the result to
table_tuple_update() as a new modified_idx_attrs argument.  It compares
every indexed attribute, not just those the UPDATE targets:
ExecGetAllUpdatedCols() names the targeted columns, and misses a column
a BEFORE ROW trigger changes with heap_modify_tuple() (as
tsvector_update_trigger() does), while logical replication apply and
REPACK (CONCURRENTLY) apply hand over whole tuples.  The comparison is
the binary comparison heap_update() has always made, so an unchanged
toasted value still compares equal without being fetched.

heapam_tuple_update() derives the HOT decision and the lock mode from
the set, through the new HeapUpdateHotAllowable() and
HeapUpdateDetermineLockmode(), and passes both to heap_update().
simple_heap_update() has no slots: it fetches the old tuple and computes
the same set with HeapUpdateModifiedIdxAttrs(), the remainder of
HeapDetermineColumnsInfo().  heap_update() keeps the replica identity
decision, which needs the old tuple on the page.  The decisions about
HOT, lock mode and replica identity do not change; hot_updates' expected
output is unchanged.

INDEX_ATTR_BITMAP_HOT_BLOCKING becomes INDEX_ATTR_BITMAP_INDEXED, the
attributes referenced by any index, which is the set to compare.
INDEX_ATTR_BITMAP_SUMMARIZED narrows from attributes referenced by a
summarizing index to attributes referenced only by summarizing indexes.
heap_update() consulted it only after finding that no HOT-blocking
attribute changed, where the two meanings agree; now an update is
summarizing-only exactly when its modified set is a subset of it.

The syscache-update-pruned isolation test waits at an injection point
before simple_heap_update() pins the old tuple's buffer, which now
happens before heap_update(); the point moves with it.

Discussion: https://postgr.es/m/78574B24-BE0A-42C5-8075-3FA9FA63B8FC@amazon.com
---
 src/backend/access/heap/heapam.c              | 463 ++++++++++++------
 src/backend/access/heap/heapam_handler.c      |  31 +-
 src/backend/access/table/tableam.c            |   5 +-
 src/backend/commands/repack.c                 |   9 +-
 src/backend/executor/execReplication.c        |   9 +-
 src/backend/executor/nodeModifyTable.c        | 102 +++-
 src/backend/utils/cache/relcache.c            |  44 +-
 src/include/access/heapam.h                   |  10 +-
 src/include/access/tableam.h                  |  12 +-
 src/include/executor/executor.h               |   3 +
 src/include/utils/rel.h                       |   2 +-
 src/include/utils/relcache.h                  |   2 +-
 .../expected/syscache-update-pruned.out       |  12 +-
 .../specs/syscache-update-pruned.spec         |   6 +-
 14 files changed, 499 insertions(+), 211 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 5c1eaadd442..509744613a5 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -37,6 +37,8 @@
 #include "access/multixact.h"
 #include "access/subtrans.h"
 #include "access/syncscan.h"
+#include "access/sysattr.h"
+#include "access/tableam.h"
 #include "access/valid.h"
 #include "access/visibilitymap.h"
 #include "access/xloginsert.h"
@@ -44,8 +46,11 @@
 #include "catalog/pg_database_d.h"
 #include "commands/vacuum.h"
 #include "executor/instrument_node.h"
+#include "executor/tuptable.h"
+#include "nodes/lockoptions.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/buf.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
 #include "storage/proc.h"
@@ -53,6 +58,7 @@
 #include "utils/datum.h"
 #include "utils/injection_point.h"
 #include "utils/inval.h"
+#include "utils/relcache.h"
 #include "utils/spccache.h"
 #include "utils/syscache.h"
 
@@ -71,11 +77,8 @@ static void check_lock_if_inplace_updateable_rel(Relation relation,
 												 HeapTuple newtup);
 static void check_inplace_rel_lock(HeapTuple oldtup);
 #endif
-static Bitmapset *HeapDetermineColumnsInfo(Relation relation,
-										   Bitmapset *interesting_cols,
-										   Bitmapset *external_cols,
-										   HeapTuple oldtup, HeapTuple newtup,
-										   bool *has_external);
+static Bitmapset *HeapUpdateModifiedIdxAttrs(Relation relation,
+											 HeapTuple oldtup, HeapTuple newtup);
 static bool heap_acquire_tuplock(Relation relation, const ItemPointerData *tid,
 								 LockTupleMode mode, LockWaitPolicy wait_policy,
 								 bool *have_tuple_lock);
@@ -3258,7 +3261,7 @@ simple_heap_delete(Relation relation, const ItemPointerData *tid)
  *	heap_update - replace a tuple
  *
  * See table_tuple_update() for an explanation of the parameters, except that
- * this routine directly takes a tuple rather than a slot.
+ * this routine directly takes a heap tuple rather than a slot.
  *
  * In the failure cases, the routine fills *tmfd with the tuple's t_ctid,
  * t_xmax (resolving a possible MultiXact, if necessary), and t_cmax (the last
@@ -3268,17 +3271,13 @@ simple_heap_delete(Relation relation, const ItemPointerData *tid)
 TM_Result
 heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 			CommandId cid, uint32 options, Snapshot crosscheck, bool wait,
-			TM_FailureData *tmfd, LockTupleMode *lockmode,
-			TU_UpdateIndexes *update_indexes)
+			TM_FailureData *tmfd, const LockTupleMode lockmode,
+			const Bitmapset *modified_idx_attrs, const bool hot_allowed)
 {
 	TM_Result	result;
 	TransactionId xid = GetCurrentTransactionId();
-	Bitmapset  *hot_attrs;
-	Bitmapset  *sum_attrs;
-	Bitmapset  *key_attrs;
-	Bitmapset  *id_attrs;
-	Bitmapset  *interesting_attrs;
-	Bitmapset  *modified_attrs;
+	Bitmapset  *idx_attrs,
+			   *id_attrs;
 	ItemId		lp;
 	HeapTupleData oldtup;
 	HeapTuple	heaptup;
@@ -3301,7 +3300,6 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 	bool		have_tuple_lock = false;
 	bool		iscombo;
 	bool		use_hot_update = false;
-	bool		summarized_update = false;
 	bool		key_intact;
 	bool		clear_all_visible = false;
 	bool		clear_all_visible_new = false;
@@ -3309,7 +3307,7 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 	bool		vmbuffer_new_modified = false;
 	bool		checked_lockers;
 	bool		locker_remains;
-	bool		id_has_external = false;
+	bool		rep_id_key_required = false;
 	TransactionId xmax_new_tuple,
 				xmax_old_tuple;
 	uint16		infomask_old_tuple,
@@ -3340,33 +3338,18 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 #endif
 
 	/*
-	 * Fetch the list of attributes to be checked for various operations.
-	 *
-	 * For HOT considerations, this is wasted effort if we fail to update or
-	 * have to put the new tuple on a different page.  But we must compute the
-	 * list before obtaining buffer lock --- in the worst case, if we are
-	 * doing an update on one of the relevant system catalogs, we could
-	 * deadlock if we try to fetch the list later.  In any case, the relcache
-	 * caches the data so this is usually pretty cheap.
-	 *
-	 * We also need columns used by the replica identity and columns that are
-	 * considered the "key" of rows in the table.
+	 * Fetch the attributes used across all indexes on this relation as well
+	 * as the replica identity and columns.
 	 *
-	 * Note that we get copies of each bitmap, so we need not worry about
-	 * relcache flush happening midway through.
-	 */
-	hot_attrs = RelationGetIndexAttrBitmap(relation,
-										   INDEX_ATTR_BITMAP_HOT_BLOCKING);
-	sum_attrs = RelationGetIndexAttrBitmap(relation,
-										   INDEX_ATTR_BITMAP_SUMMARIZED);
-	key_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_KEY);
-	id_attrs = RelationGetIndexAttrBitmap(relation,
-										  INDEX_ATTR_BITMAP_IDENTITY_KEY);
-	interesting_attrs = NULL;
-	interesting_attrs = bms_add_members(interesting_attrs, hot_attrs);
-	interesting_attrs = bms_add_members(interesting_attrs, sum_attrs);
-	interesting_attrs = bms_add_members(interesting_attrs, key_attrs);
-	interesting_attrs = bms_add_members(interesting_attrs, id_attrs);
+	 * Note: We must compute the list before obtaining buffer lock. In the
+	 * worst case, if we are doing an update on one of the relevant system
+	 * catalogs, we could deadlock if we try to fetch the list later. Keep in
+	 * mind that relcache returns copies of each bitmap, so we need not worry
+	 * about relcache flush happening midway through, but we do need to free
+	 * them.
+	 */
+	idx_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_INDEXED);
+	id_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_IDENTITY_KEY);
 
 	block = ItemPointerGetBlockNumber(otid);
 	INJECTION_POINT("heap_update-before-pin", NULL);
@@ -3420,20 +3403,17 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 		tmfd->ctid = *otid;
 		tmfd->xmax = InvalidTransactionId;
 		tmfd->cmax = InvalidCommandId;
-		*update_indexes = TU_None;
 
-		bms_free(hot_attrs);
-		bms_free(sum_attrs);
-		bms_free(key_attrs);
 		bms_free(id_attrs);
-		/* modified_attrs not yet initialized */
-		bms_free(interesting_attrs);
+		bms_free(idx_attrs);
+		/* modified_idx_attrs is owned by the caller, don't free it */
+
 		return TM_Deleted;
 	}
 
 	/*
-	 * Fill in enough data in oldtup for HeapDetermineColumnsInfo to work
-	 * properly.
+	 * Fill in enough data in oldtup to determine replica identity attribute
+	 * requirements.
 	 */
 	oldtup.t_tableOid = RelationGetRelid(relation);
 	oldtup.t_data = (HeapTupleHeader) PageGetItem(page, lp);
@@ -3444,16 +3424,59 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 	newtup->t_tableOid = RelationGetRelid(relation);
 
 	/*
-	 * Determine columns modified by the update.  Additionally, identify
-	 * whether any of the unmodified replica identity key attributes in the
-	 * old tuple is externally stored or not.  This is required because for
-	 * such attributes the flattened value won't be WAL logged as part of the
-	 * new tuple so we must include it as part of the old_key_tuple.  See
-	 * ExtractReplicaIdentity.
+	 * ExtractReplicaIdentity() needs to know if a modified indexed attrbute
+	 * is used as a replica indentity or if any of the replica identity
+	 * attributes are referenced in an index, unmodified, and are stored
+	 * externally in the old tuple being replaced.  In those cases it may be
+	 * necessary to WAL log them to so they are available to replicas.
 	 */
-	modified_attrs = HeapDetermineColumnsInfo(relation, interesting_attrs,
-											  id_attrs, &oldtup,
-											  newtup, &id_has_external);
+	rep_id_key_required = bms_overlap(modified_idx_attrs, id_attrs);
+	if (!rep_id_key_required)
+	{
+		Bitmapset  *attrs;
+		TupleDesc	tupdesc = RelationGetDescr(relation);
+		int			attidx = -1;
+
+		/*
+		 * Reduce the set under review to only the unmodified indexed replica
+		 * identity key attributes.  idx_attrs is copied (by bms_difference())
+		 * not modified here.
+		 */
+		attrs = bms_difference(idx_attrs, modified_idx_attrs);
+		attrs = bms_int_members(attrs, id_attrs);
+
+		while ((attidx = bms_next_member(attrs, attidx)) >= 0)
+		{
+			/*
+			 * attidx is zero-based, attrnum is the normal attribute number
+			 */
+			AttrNumber	attrnum = attidx + FirstLowInvalidHeapAttributeNumber;
+			Datum		value;
+			bool		isnull;
+
+			/*
+			 * System attributes are not added into INDEX_ATTR_BITMAP_INDEXED
+			 * bitmap by relcache.
+			 */
+			Assert(attrnum > 0);
+
+			value = heap_getattr(&oldtup, attrnum, tupdesc, &isnull);
+
+			/* No need to check attributes that can't be stored externally */
+			if (isnull ||
+				TupleDescCompactAttr(tupdesc, attrnum - 1)->attlen != -1)
+				continue;
+
+			/* Check if the old tuple's attribute is stored externally */
+			if (VARATT_IS_EXTERNAL((struct varlena *) DatumGetPointer(value)))
+			{
+				rep_id_key_required = true;
+				break;
+			}
+		}
+
+		bms_free(attrs);
+	}
 
 	/*
 	 * If we're not updating any "key" column, we can grab a weaker lock type.
@@ -3466,9 +3489,8 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 	 * is updates that don't manipulate key columns, not those that
 	 * serendipitously arrive at the same key values.
 	 */
-	if (!bms_overlap(modified_attrs, key_attrs))
+	if (lockmode == LockTupleNoKeyExclusive)
 	{
-		*lockmode = LockTupleNoKeyExclusive;
 		mxact_status = MultiXactStatusNoKeyUpdate;
 		key_intact = true;
 
@@ -3485,7 +3507,7 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 	}
 	else
 	{
-		*lockmode = LockTupleExclusive;
+		Assert(lockmode == LockTupleExclusive);
 		mxact_status = MultiXactStatusUpdate;
 		key_intact = false;
 	}
@@ -3564,7 +3586,7 @@ l2:
 			bool		current_is_member = false;
 
 			if (DoesMultiXactIdConflict((MultiXactId) xwait, infomask,
-										*lockmode, &current_is_member))
+										lockmode, &current_is_member))
 			{
 				LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
 
@@ -3573,7 +3595,7 @@ l2:
 				 * requesting a lock and already have one; avoids deadlock).
 				 */
 				if (!current_is_member)
-					heap_acquire_tuplock(relation, &(oldtup.t_self), *lockmode,
+					heap_acquire_tuplock(relation, &(oldtup.t_self), lockmode,
 										 LockWaitBlock, &have_tuple_lock);
 
 				/* wait for multixact */
@@ -3658,7 +3680,7 @@ l2:
 			 * lock.
 			 */
 			LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
-			heap_acquire_tuplock(relation, &(oldtup.t_self), *lockmode,
+			heap_acquire_tuplock(relation, &(oldtup.t_self), lockmode,
 								 LockWaitBlock, &have_tuple_lock);
 			XactLockTableWait(xwait, relation, &oldtup.t_self,
 							  XLTW_Update);
@@ -3718,17 +3740,14 @@ l2:
 			tmfd->cmax = InvalidCommandId;
 		UnlockReleaseBuffer(buffer);
 		if (have_tuple_lock)
-			UnlockTupleTuplock(relation, &(oldtup.t_self), *lockmode);
+			UnlockTupleTuplock(relation, &(oldtup.t_self), lockmode);
 		if (vmbuffer != InvalidBuffer)
 			ReleaseBuffer(vmbuffer);
-		*update_indexes = TU_None;
 
-		bms_free(hot_attrs);
-		bms_free(sum_attrs);
-		bms_free(key_attrs);
 		bms_free(id_attrs);
-		bms_free(modified_attrs);
-		bms_free(interesting_attrs);
+		bms_free(idx_attrs);
+		/* modified_idx_attrs is owned by the caller, don't free it */
+
 		return result;
 	}
 
@@ -3758,7 +3777,7 @@ l2:
 	compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(oldtup.t_data),
 							  oldtup.t_data->t_infomask,
 							  oldtup.t_data->t_infomask2,
-							  xid, *lockmode, true,
+							  xid, lockmode, true,
 							  &xmax_old_tuple, &infomask_old_tuple,
 							  &infomask2_old_tuple);
 
@@ -3875,7 +3894,7 @@ l2:
 		compute_new_xmax_infomask(HeapTupleHeaderGetRawXmax(oldtup.t_data),
 								  oldtup.t_data->t_infomask,
 								  oldtup.t_data->t_infomask2,
-								  xid, *lockmode, false,
+								  xid, lockmode, false,
 								  &xmax_lock_old_tuple, &infomask_lock_old_tuple,
 								  &infomask2_lock_old_tuple);
 
@@ -4073,20 +4092,8 @@ l2:
 		 * to do a HOT update.  Check if any of the index columns have been
 		 * changed.
 		 */
-		if (!bms_overlap(modified_attrs, hot_attrs))
-		{
+		if (hot_allowed)
 			use_hot_update = true;
-
-			/*
-			 * If none of the columns that are used in hot-blocking indexes
-			 * were updated, we can apply HOT, but we do still need to check
-			 * if we need to update the summarizing indexes, and update those
-			 * indexes if the columns were updated, or we may fail to detect
-			 * e.g. value bound changes in BRIN minmax indexes.
-			 */
-			if (bms_overlap(modified_attrs, sum_attrs))
-				summarized_update = true;
-		}
 	}
 	else
 	{
@@ -4102,8 +4109,7 @@ l2:
 	 * columns are modified or it has external data.
 	 */
 	old_key_tuple = ExtractReplicaIdentity(relation, &oldtup,
-										   bms_overlap(modified_attrs, id_attrs) ||
-										   id_has_external,
+										   rep_id_key_required,
 										   &old_key_copied);
 
 	clear_all_visible = PageIsAllVisible(page);
@@ -4331,7 +4337,7 @@ l2:
 	 * Release the lmgr tuple lock, if we had it.
 	 */
 	if (have_tuple_lock)
-		UnlockTupleTuplock(relation, &(oldtup.t_self), *lockmode);
+		UnlockTupleTuplock(relation, &(oldtup.t_self), lockmode);
 
 	pgstat_count_heap_update(relation, use_hot_update, newbuf != buffer);
 
@@ -4345,31 +4351,12 @@ l2:
 		heap_freetuple(heaptup);
 	}
 
-	/*
-	 * If it is a HOT update, the update may still need to update summarized
-	 * indexes, lest we fail to update those summaries and get incorrect
-	 * results (for example, minmax bounds of the block may change with this
-	 * update).
-	 */
-	if (use_hot_update)
-	{
-		if (summarized_update)
-			*update_indexes = TU_Summarizing;
-		else
-			*update_indexes = TU_None;
-	}
-	else
-		*update_indexes = TU_All;
-
 	if (old_key_tuple != NULL && old_key_copied)
 		heap_freetuple(old_key_tuple);
 
-	bms_free(hot_attrs);
-	bms_free(sum_attrs);
-	bms_free(key_attrs);
 	bms_free(id_attrs);
-	bms_free(modified_attrs);
-	bms_free(interesting_attrs);
+	bms_free(idx_attrs);
+	/* modified_idx_attrs is owned by the caller, don't free it */
 
 	return TM_Ok;
 }
@@ -4542,28 +4529,115 @@ heap_attr_equals(TupleDesc tupdesc, int attrnum, Datum value1, Datum value2,
 }
 
 /*
- * Check which columns are being updated.
- *
- * Given an updated tuple, determine (and return into the output bitmapset),
- * from those listed as interesting, the set of columns that changed.
- *
- * has_external indicates if any of the unmodified attributes (from those
- * listed as interesting) of the old tuple is a member of external_cols and is
- * stored externally.
+ * HOT updates are possible when either: a) there are no modified indexed
+ * attributes, or b) the modified attributes are all on summarizing indexes.
+ * Later, in heap_update(), we can choose to perform a HOT update if there is
+ * space on the page for the new tuple and the following code has determined
+ * that HOT is allowed.
+ */
+bool
+HeapUpdateHotAllowable(Relation relation, const Bitmapset *modified_idx_attrs,
+					   bool *summarized_only)
+{
+	bool		hot_allowed;
+
+	/*
+	 * Let's be optimistic and start off by assuming the best case, no indexes
+	 * need updating and HOT is allowable.
+	 */
+	hot_allowed = true;
+	*summarized_only = false;
+
+	/*
+	 * Check for case (a); when there are no modified index attributes HOT is
+	 * allowed.
+	 */
+	if (bms_is_empty(modified_idx_attrs))
+		hot_allowed = true;
+	else
+	{
+		Bitmapset  *sum_attrs = RelationGetIndexAttrBitmap(relation,
+														   INDEX_ATTR_BITMAP_SUMMARIZED);
+
+		/*
+		 * At least one index attribute was modified, but is this case (b)
+		 * where all the modified index attributes are only used by
+		 * summarizing indexes?  If it is, then we need to update those
+		 * indexes, but this update can still be considered heap-only (HOT)
+		 * and avoid updating any non-summarizing indexes on the relation.
+		 */
+		if (bms_is_subset(modified_idx_attrs, sum_attrs))
+		{
+			hot_allowed = true;
+			*summarized_only = true;
+		}
+		else
+		{
+			/*
+			 * Now we know a) one or more indexed attributes were modified
+			 * (changed value, not just referenced within the UPDATE) and that
+			 * b) at least one of those attributes is used by a
+			 * non-summarizing index. HOT is not allowed.
+			 */
+			hot_allowed = false;
+		}
+
+		bms_free(sum_attrs);
+	}
+
+	return hot_allowed;
+}
+
+/*
+ * If we're not updating any attributes used when forming the index keys we can
+ * grab a weaker lock type. This allows for more concurrency when we are
+ * running simultaneously with foreign key checks.
+ */
+LockTupleMode
+HeapUpdateDetermineLockmode(Relation relation, const Bitmapset *modified_idx_attrs)
+{
+	LockTupleMode lockmode = LockTupleExclusive;
+
+	Bitmapset  *key_attrs = RelationGetIndexAttrBitmap(relation,
+													   INDEX_ATTR_BITMAP_KEY);
+
+	if (!bms_overlap(modified_idx_attrs, key_attrs))
+		lockmode = LockTupleNoKeyExclusive;
+
+	bms_free(key_attrs);
+
+	return lockmode;
+}
+
+/*
+ * Return a Bitmapset that contains the set of modified (changed) indexed
+ * attributes between oldtup and newtup.
  */
 static Bitmapset *
-HeapDetermineColumnsInfo(Relation relation,
-						 Bitmapset *interesting_cols,
-						 Bitmapset *external_cols,
-						 HeapTuple oldtup, HeapTuple newtup,
-						 bool *has_external)
+HeapUpdateModifiedIdxAttrs(Relation relation, HeapTuple oldtup, HeapTuple newtup)
 {
 	int			attidx;
-	Bitmapset  *modified = NULL;
+	Bitmapset  *attrs,
+			   *modified_idx_attrs = NULL;
 	TupleDesc	tupdesc = RelationGetDescr(relation);
 
+	/* Get the set of all attributes across all indexes for this relation */
+	attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_INDEXED);
+
+	/* No indexed attributes, we're done */
+	if (bms_is_empty(attrs))
+		return NULL;
+
+	/*
+	 * This heap update function is used outside the executor and so unlike
+	 * heapam_tuple_update() where there is ResultRelInfo and EState to
+	 * provide the concise set of attributes that might have been modified
+	 * (via ExecGetAllUpdatedCols()) we simply check all indexed attributes to
+	 * find the subset that changed value.  That's the "modified indexed
+	 * attributes" or "modified_idx_attrs".
+	 */
 	attidx = -1;
-	while ((attidx = bms_next_member(interesting_cols, attidx)) >= 0)
+	while ((attidx = bms_next_member(attrs, attidx)) >= 0)
 	{
 		/* attidx is zero-based, attrnum is the normal attribute number */
 		AttrNumber	attrnum = attidx + FirstLowInvalidHeapAttributeNumber;
@@ -4579,7 +4653,7 @@ HeapDetermineColumnsInfo(Relation relation,
 		 */
 		if (attrnum == 0)
 		{
-			modified = bms_add_member(modified, attidx);
+			modified_idx_attrs = bms_add_member(modified_idx_attrs, attidx);
 			continue;
 		}
 
@@ -4592,7 +4666,7 @@ HeapDetermineColumnsInfo(Relation relation,
 		{
 			if (attrnum != TableOidAttributeNumber)
 			{
-				modified = bms_add_member(modified, attidx);
+				modified_idx_attrs = bms_add_member(modified_idx_attrs, attidx);
 				continue;
 			}
 		}
@@ -4608,29 +4682,12 @@ HeapDetermineColumnsInfo(Relation relation,
 
 		if (!heap_attr_equals(tupdesc, attrnum, value1,
 							  value2, isnull1, isnull2))
-		{
-			modified = bms_add_member(modified, attidx);
-			continue;
-		}
-
-		/*
-		 * No need to check attributes that can't be stored externally. Note
-		 * that system attributes can't be stored externally.
-		 */
-		if (attrnum < 0 || isnull1 ||
-			TupleDescCompactAttr(tupdesc, attrnum - 1)->attlen != -1)
-			continue;
-
-		/*
-		 * Check if the old tuple's attribute is stored externally and is a
-		 * member of external_cols.
-		 */
-		if (VARATT_IS_EXTERNAL((varlena *) DatumGetPointer(value1)) &&
-			bms_is_member(attidx, external_cols))
-			*has_external = true;
+			modified_idx_attrs = bms_add_member(modified_idx_attrs, attidx);
 	}
 
-	return modified;
+	bms_free(attrs);
+
+	return modified_idx_attrs;
 }
 
 /*
@@ -4648,12 +4705,106 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
 	TM_Result	result;
 	TM_FailureData tmfd;
 	LockTupleMode lockmode;
+	TupleTableSlot *slot;
+	BufferHeapTupleTableSlot *bslot;
+	HeapTuple	oldtup;
+	bool		shouldFree = true;
+	Bitmapset  *idx_attrs,
+			   *modified_idx_attrs;
+	bool		hot_allowed,
+				summarized_only;
+	Buffer		buffer;
 
-	result = heap_update(relation, otid, tup,
-						 GetCurrentCommandId(true), 0,
-						 InvalidSnapshot,
-						 true /* wait for commit */ ,
-						 &tmfd, &lockmode, update_indexes);
+	Assert(ItemPointerIsValid(otid));
+
+	/*
+	 * Fetch this bitmap of interesting attributes from relcache before
+	 * obtaining a buffer lock because if we are doing an update on one of the
+	 * relevant system catalogs we could deadlock if we try to fetch them
+	 * later on. Relcache will return copies of each bitmap, so we need not
+	 * worry about relcache flush happening midway through this operation.
+	 */
+	idx_attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_INDEXED);
+
+	INJECTION_POINT("simple_heap_update-before-pin", NULL);
+
+	/*
+	 * To update a heap tuple we need to find the set of modified indexed
+	 * attributes ("modified_idx_attrs") and use that to determine if a HOT
+	 * update is allowable or not. When updating heap tuples via execution of
+	 * UPDATE statements this set is constructed before calling into the table
+	 * AM's update function by ExecUpdateModifiedIdxAttrs() which compares the
+	 * old/new TupleTableSlots.
+	 *
+	 * Here things are a bit different, we have the old TID and the new tuple,
+	 * not two TupleTableSlots, but we still need to construct a similar
+	 * bitmap so as to be able to know if HOT updates are allowed or not.
+	 *
+	 * To do that we first have to fetch the old tuple itself, but because
+	 * heapam_fetch_row_version() is static, we replicate in part that code
+	 * here.
+	 *
+	 * This is a bit repetitive because heap_update() will again find and form
+	 * the old HeapTuple from the old TID and in most cases the callers
+	 * (ignoring extensions, are always catalog tuple updates) already had the
+	 * set of changed attributes (the "replaces" array), but for now this
+	 * minor repetition of work is necessary.
+	 */
+	slot = MakeTupleTableSlot(RelationGetDescr(relation), &TTSOpsBufferHeapTuple, 0);
+	bslot = (BufferHeapTupleTableSlot *) slot;
+
+	/*
+	 * Set the TID in the slot and then fetch the old tuple so we can examine
+	 * it
+	 */
+	bslot->base.tupdata.t_self = *otid;
+	if (!heap_fetch(relation, SnapshotAny, &bslot->base.tupdata, &buffer, false))
+	{
+		/*
+		 * heap_update() checks for !ItemIdIsNormal(lp) and will return false
+		 * in those cases.
+		 */
+		Assert(RelationSupportsSysCache(RelationGetRelid(relation)));
+
+		*update_indexes = TU_None;
+
+		/* modified_idx_attrs not yet initialized */
+		bms_free(idx_attrs);
+		ExecDropSingleTupleTableSlot(slot);
+
+		elog(ERROR, "tuple concurrently deleted");
+
+		return;
+	}
+
+	Assert(buffer != InvalidBuffer);
+
+	/* Store in slot, transferring existing pin */
+	ExecStorePinnedBufferHeapTuple(&bslot->base.tupdata, slot, buffer);
+	oldtup = ExecFetchSlotHeapTuple(slot, false, &shouldFree);
+
+	modified_idx_attrs = HeapUpdateModifiedIdxAttrs(relation, oldtup, tup);
+	lockmode = HeapUpdateDetermineLockmode(relation, modified_idx_attrs);
+	hot_allowed = HeapUpdateHotAllowable(relation, modified_idx_attrs, &summarized_only);
+
+	result = heap_update(relation, otid, tup, GetCurrentCommandId(true), 0,
+						 InvalidSnapshot, true /* wait for commit */ ,
+						 &tmfd, lockmode, modified_idx_attrs, hot_allowed);
+
+	if (shouldFree)
+		heap_freetuple(oldtup);
+
+	ExecDropSingleTupleTableSlot(slot);
+	bms_free(idx_attrs);
+
+	/*
+	 * Decide whether new index entries are needed for the tuple
+	 *
+	 * If the update is not HOT, we must update all indexes. If the update is
+	 * HOT, it could be that we updated summarized columns, so we either
+	 * update only summarized indexes, or none at all.
+	 */
+	*update_indexes = TU_None;
 	switch (result)
 	{
 		case TM_SelfModified:
@@ -4663,6 +4814,10 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
 
 		case TM_Ok:
 			/* done successfully */
+			if (!HeapTupleIsHeapOnly(tup))
+				*update_indexes = TU_All;
+			else if (summarized_only)
+				*update_indexes = TU_Summarizing;
 			break;
 
 		case TM_Updated:
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 7d2065dec37..864d7e1c976 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -27,7 +27,6 @@
 #include "access/syncscan.h"
 #include "access/tableam.h"
 #include "access/tsmapi.h"
-#include "access/visibilitymap.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "catalog/index.h"
@@ -218,20 +217,27 @@ static TM_Result
 heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 					CommandId cid, uint32 options,
 					Snapshot snapshot, Snapshot crosscheck,
-					bool wait, TM_FailureData *tmfd,
-					LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes)
+					bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode,
+					const Bitmapset *modified_idx_attrs, TU_UpdateIndexes *update_indexes)
 {
 	bool		shouldFree = true;
 	HeapTuple	tuple = ExecFetchSlotHeapTuple(slot, true, &shouldFree);
+	bool		hot_allowed;
+	bool		summarized_only;
 	TM_Result	result;
 
+	Assert(ItemPointerIsValid(otid));
+
+	hot_allowed = HeapUpdateHotAllowable(relation, modified_idx_attrs, &summarized_only);
+	*lockmode = HeapUpdateDetermineLockmode(relation, modified_idx_attrs);
+
 	/* Update the tuple with table oid */
 	slot->tts_tableOid = RelationGetRelid(relation);
 	tuple->t_tableOid = slot->tts_tableOid;
 
 	result = heap_update(relation, otid, tuple, cid, options,
 						 crosscheck, wait,
-						 tmfd, lockmode, update_indexes);
+						 tmfd, *lockmode, modified_idx_attrs, hot_allowed);
 	ItemPointerCopy(&tuple->t_self, &slot->tts_tid);
 
 	/*
@@ -244,16 +250,17 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 	 * HOT, it could be that we updated summarized columns, so we either
 	 * update only summarized indexes, or none at all.
 	 */
-	if (result != TM_Ok)
+	*update_indexes = TU_None;
+	if (result == TM_Ok)
 	{
-		Assert(*update_indexes == TU_None);
-		*update_indexes = TU_None;
+		if (HeapTupleIsHeapOnly(tuple))
+		{
+			if (summarized_only)
+				*update_indexes = TU_Summarizing;
+		}
+		else
+			*update_indexes = TU_All;
 	}
-	else if (!HeapTupleIsHeapOnly(tuple))
-		Assert(*update_indexes == TU_All);
-	else
-		Assert((*update_indexes == TU_Summarizing) ||
-			   (*update_indexes == TU_None));
 
 	if (shouldFree)
 		pfree(tuple);
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 7aaa8d87150..5c155a25f99 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -323,6 +323,7 @@ void
 simple_table_tuple_update(Relation rel, ItemPointer otid,
 						  TupleTableSlot *slot,
 						  Snapshot snapshot,
+						  const Bitmapset *modified_idx_attrs,
 						  TU_UpdateIndexes *update_indexes)
 {
 	TM_Result	result;
@@ -333,7 +334,9 @@ simple_table_tuple_update(Relation rel, ItemPointer otid,
 								GetCurrentCommandId(true),
 								0, snapshot, InvalidSnapshot,
 								true /* wait for commit */ ,
-								&tmfd, &lockmode, update_indexes);
+								&tmfd, &lockmode,
+								modified_idx_attrs,
+								update_indexes);
 
 	switch (result)
 	{
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index a584e138ada..0643fe81e65 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2876,6 +2876,11 @@ apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple,
 	TM_FailureData tmfd;
 	TU_UpdateIndexes update_indexes;
 	TM_Result	res;
+	Bitmapset  *modified_idx_attrs;
+
+	modified_idx_attrs = ExecUpdateModifiedIdxAttrs(chgcxt->cc_rri,
+													ondisk_tuple,
+													spilled_tuple);
 
 	/*
 	 * Carry out the update, skipping logical decoding for it.
@@ -2886,7 +2891,9 @@ apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple,
 							 InvalidSnapshot,
 							 InvalidSnapshot,
 							 false,
-							 &tmfd, &lockmode, &update_indexes);
+							 &tmfd, &lockmode,
+							 modified_idx_attrs, &update_indexes);
+	bms_free(modified_idx_attrs);
 	if (res != TM_Ok)
 		ereport(ERROR,
 				errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 18da4fe08f6..904d388a19c 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -33,6 +33,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/relcache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
@@ -931,6 +932,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 	bool		skip_tuple = false;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	ItemPointer tid = &(searchslot->tts_tid);
+	Bitmapset  *modified_idx_attrs;
 
 	/*
 	 * We support only non-system tables, with
@@ -969,8 +971,13 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
 		if (rel->rd_rel->relispartition)
 			ExecPartitionCheck(resultRelInfo, slot, estate, true);
 
+		modified_idx_attrs = ExecUpdateModifiedIdxAttrs(resultRelInfo,
+														searchslot, slot);
+
 		simple_table_tuple_update(rel, tid, slot, estate->es_snapshot,
-								  &update_indexes);
+								  modified_idx_attrs, &update_indexes);
+		bms_free(modified_idx_attrs);
+
 
 		conflictindexes = resultRelInfo->ri_onConflictArbiterIndexes;
 
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 558ceece9bb..ed651056080 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -18,6 +18,7 @@
  *		ExecModifyTable		- retrieve the next tuple from the node
  *		ExecEndModifyTable	- shut down the ModifyTable node
  *		ExecReScanModifyTable - rescan the ModifyTable node
+ *		ExecUpdateModifiedIdxAttrs - find set of updated indexed columns
  *
  *	 NOTES
  *		The ModifyTable node receives input from its outerPlan, which is
@@ -56,6 +57,7 @@
 #include "access/htup_details.h"
 #include "access/tableam.h"
 #include "access/tupconvert.h"
+#include "access/tupdesc.h"
 #include "access/xact.h"
 #include "commands/trigger.h"
 #include "executor/execPartition.h"
@@ -192,6 +194,76 @@ static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
 										   ResultRelInfo *resultRelInfo,
 										   bool canSetTag);
 
+/*
+ * ExecUpdateModifiedIdxAttrs
+ *
+ * Return the set of indexed attributes whose values differ between old_tts
+ * and new_tts, using the FirstLowInvalidHeapAttributeNumber convention.  The
+ * result describes this one pair of tuples; the caller frees it.
+ *
+ * All indexed attributes are compared, not just those the UPDATE targets.
+ * ExecGetAllUpdatedCols() names the columns an UPDATE targets, not the values
+ * that changed: a BEFORE ROW trigger can change an indexed column with
+ * heap_modify_tuple() (see tsvector_update_trigger()), and logical
+ * replication and REPACK apply hand us whole tuples.
+ *
+ * Values are compared byte for byte as stored, as heap_update() always has.
+ * An unchanged toasted value keeps its toast pointer, so it compares equal
+ * without being fetched.  Two values that are equal but stored differently
+ * count as changed; that costs an index entry, never a missed one.
+ *
+ * simple_heap_update() makes the same determination from a TID and a
+ * HeapTuple in HeapUpdateModifiedIdxAttrs().
+ */
+Bitmapset *
+ExecUpdateModifiedIdxAttrs(ResultRelInfo *resultRelInfo,
+						   TupleTableSlot *old_tts,
+						   TupleTableSlot *new_tts)
+{
+	Relation	relation = resultRelInfo->ri_RelationDesc;
+	TupleDesc	tupdesc = RelationGetDescr(relation);
+	Bitmapset  *attrs;
+	int			attidx = -1;
+
+	if (resultRelInfo->ri_NumIndices == 0)
+		return NULL;
+
+	attrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_INDEXED);
+
+	while ((attidx = bms_next_member(attrs, attidx)) >= 0)
+	{
+		AttrNumber	attrnum = attidx + FirstLowInvalidHeapAttributeNumber;
+		CompactAttribute *att;
+		Datum		value1,
+					value2;
+		bool		null1,
+					null2;
+
+		/*
+		 * A whole-row reference is "changed" (it could only be unchanged
+		 * after a no-op update), and so is any system column other than
+		 * tableoid, which need not be set yet in the new tuple.
+		 */
+		if (attrnum <= 0)
+		{
+			if (attrnum == TableOidAttributeNumber)
+				attrs = bms_del_member(attrs, attidx);
+			continue;
+		}
+
+		att = TupleDescCompactAttr(tupdesc, attrnum - 1);
+		value1 = slot_getattr(old_tts, attrnum, &null1);
+		value2 = slot_getattr(new_tts, attrnum, &null2);
+
+		if (null1 != null2)
+			continue;
+		if (null1 ||
+			datumIsEqual(value1, value2, att->attbyval, att->attlen))
+			attrs = bms_del_member(attrs, attidx);
+	}
+
+	return attrs;
+}
 
 /*
  * Verify that the tuples to be produced by INSERT match the
@@ -2203,14 +2275,17 @@ ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo,
  */
 static TM_Result
 ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
-			  ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot,
-			  bool canSetTag, UpdateContext *updateCxt)
+			  ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *oldSlot,
+			  TupleTableSlot *slot, bool canSetTag, UpdateContext *updateCxt)
 {
 	EState	   *estate = context->estate;
 	Relation	resultRelationDesc = resultRelInfo->ri_RelationDesc;
 	bool		partition_constraint_failed;
 	TM_Result	result;
 
+	/* The set of modified indexed attributes that trigger new index entries */
+	Bitmapset  *modified_idx_attrs = NULL;
+
 	updateCxt->crossPartUpdate = false;
 
 	/*
@@ -2327,7 +2402,16 @@ lreplace:
 		ExecConstraints(resultRelInfo, slot, estate);
 
 	/*
-	 * replace the heap tuple
+	 * Next up we need to find out the set of indexed attributes that have
+	 * changed in value and should trigger a new index tuple.  We could start
+	 * with the set of updated columns via ExecGetUpdatedCols(), but if we do
+	 * we will overlook attributes directly modified by heap_modify_tuple()
+	 * which are not known to ExecGetUpdatedCols().
+	 */
+	modified_idx_attrs = ExecUpdateModifiedIdxAttrs(resultRelInfo, oldSlot, slot);
+
+	/*
+	 * Call into the table AM to update the heap tuple.
 	 *
 	 * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check that
 	 * the row to be updated is visible to that snapshot, and throw a
@@ -2342,7 +2426,9 @@ lreplace:
 								estate->es_crosscheck_snapshot,
 								true /* wait for commit */ ,
 								&context->tmfd, &updateCxt->lockmode,
+								modified_idx_attrs,
 								&updateCxt->updateIndexes);
+	bms_free(modified_idx_attrs);
 
 	return result;
 }
@@ -2583,8 +2669,8 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		 */
 redo_act:
 		lockedtid = *tupleid;
-		result = ExecUpdateAct(context, resultRelInfo, tupleid, oldtuple, slot,
-							   canSetTag, &updateCxt);
+		result = ExecUpdateAct(context, resultRelInfo, tupleid, oldtuple, oldSlot,
+							   slot, canSetTag, &updateCxt);
 
 		/*
 		 * If ExecUpdateAct reports that a cross-partition update was done,
@@ -3435,8 +3521,8 @@ lmerge_matched:
 					Assert(oldtuple == NULL);
 
 					result = ExecUpdateAct(context, resultRelInfo, tupleid,
-										   NULL, newslot, canSetTag,
-										   &updateCxt);
+										   NULL, resultRelInfo->ri_oldTupleSlot,
+										   newslot, canSetTag, &updateCxt);
 
 					/*
 					 * As in ExecUpdate(), if ExecUpdateAct() reports that a
@@ -4587,7 +4673,7 @@ ExecModifyTable(PlanState *pstate)
 		 * For UPDATE/DELETE/MERGE, fetch the row identity info for the tuple
 		 * to be updated/deleted/merged.  For a heap relation, that's a TID;
 		 * otherwise we may have a wholerow junk attr that carries the old
-		 * tuple in toto.  Keep this in step with the part of
+		 * tuple in total.  Keep this in step with the part of
 		 * ExecInitModifyTable that sets up ri_RowIdAttNo.
 		 */
 		if (operation == CMD_UPDATE || operation == CMD_DELETE ||
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d8f04a05309..ed138a94b33 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -2475,7 +2475,7 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc)
 	bms_free(relation->rd_keyattr);
 	bms_free(relation->rd_pkattr);
 	bms_free(relation->rd_idattr);
-	bms_free(relation->rd_hotblockingattr);
+	bms_free(relation->rd_indexedattr);
 	bms_free(relation->rd_summarizedattr);
 	if (relation->rd_pubdesc)
 		pfree(relation->rd_pubdesc);
@@ -5323,8 +5323,8 @@ RelationGetIndexPredicate(Relation relation)
  *									(beware: even if PK is deferrable!)
  *	INDEX_ATTR_BITMAP_IDENTITY_KEY	Columns in the table's replica identity
  *									index (empty if FULL)
- *	INDEX_ATTR_BITMAP_HOT_BLOCKING	Columns that block updates from being HOT
- *	INDEX_ATTR_BITMAP_SUMMARIZED	Columns included in summarizing indexes
+ *	INDEX_ATTR_BITMAP_INDEXED		Columns referenced by indexes
+ *	INDEX_ATTR_BITMAP_SUMMARIZED	Columns only included in summarizing indexes
  *
  * Attribute numbers are offset by FirstLowInvalidHeapAttributeNumber so that
  * we can include system attributes (e.g., OID) in the bitmap representation.
@@ -5347,8 +5347,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 	Bitmapset  *uindexattrs;	/* columns in unique indexes */
 	Bitmapset  *pkindexattrs;	/* columns in the primary index */
 	Bitmapset  *idindexattrs;	/* columns in the replica identity */
-	Bitmapset  *hotblockingattrs;	/* columns with HOT blocking indexes */
-	Bitmapset  *summarizedattrs;	/* columns with summarizing indexes */
+	Bitmapset  *indexedattrs;	/* columns referenced by indexes */
+	Bitmapset  *summarizedattrs;	/* columns only in summarizing indexes */
 	List	   *indexoidlist;
 	List	   *newindexoidlist;
 	Oid			relpkindex;
@@ -5367,8 +5367,8 @@ RelationGetIndexAttrBitmap(Relation relation, IndexAttrBitmapKind attrKind)
 				return bms_copy(relation->rd_pkattr);
 			case INDEX_ATTR_BITMAP_IDENTITY_KEY:
 				return bms_copy(relation->rd_idattr);
-			case INDEX_ATTR_BITMAP_HOT_BLOCKING:
-				return bms_copy(relation->rd_hotblockingattr);
+			case INDEX_ATTR_BITMAP_INDEXED:
+				return bms_copy(relation->rd_indexedattr);
 			case INDEX_ATTR_BITMAP_SUMMARIZED:
 				return bms_copy(relation->rd_summarizedattr);
 			default:
@@ -5413,7 +5413,7 @@ restart:
 	uindexattrs = NULL;
 	pkindexattrs = NULL;
 	idindexattrs = NULL;
-	hotblockingattrs = NULL;
+	indexedattrs = NULL;
 	summarizedattrs = NULL;
 	foreach(l, indexoidlist)
 	{
@@ -5473,7 +5473,7 @@ restart:
 		if (indexDesc->rd_indam->amsummarizing)
 			attrs = &summarizedattrs;
 		else
-			attrs = &hotblockingattrs;
+			attrs = &indexedattrs;
 
 		/* Collect simple attribute references */
 		for (i = 0; i < indexDesc->rd_index->indnatts; i++)
@@ -5482,9 +5482,9 @@ restart:
 
 			/*
 			 * Since we have covering indexes with non-key columns, we must
-			 * handle them accurately here. non-key columns must be added into
-			 * hotblockingattrs or summarizedattrs, since they are in index,
-			 * and update shouldn't miss them.
+			 * handle them accurately here. Non-key columns must be added into
+			 * indexedattrs or summarizedattrs, since they are in index, and
+			 * update shouldn't miss them.
 			 *
 			 * Summarizing indexes do not block HOT, but do need to be updated
 			 * when the column value changes, thus require a separate
@@ -5545,12 +5545,20 @@ restart:
 		bms_free(uindexattrs);
 		bms_free(pkindexattrs);
 		bms_free(idindexattrs);
-		bms_free(hotblockingattrs);
+		bms_free(indexedattrs);
 		bms_free(summarizedattrs);
 
 		goto restart;
 	}
 
+	/*
+	 * Record what attributes are only referenced by summarizing indexes. Then
+	 * add that into the other indexed attributes to track all referenced
+	 * attributes.
+	 */
+	summarizedattrs = bms_del_members(summarizedattrs, indexedattrs);
+	indexedattrs = bms_add_members(indexedattrs, summarizedattrs);
+
 	/* Don't leak the old values of these bitmaps, if any */
 	relation->rd_attrsvalid = false;
 	bms_free(relation->rd_keyattr);
@@ -5559,8 +5567,8 @@ restart:
 	relation->rd_pkattr = NULL;
 	bms_free(relation->rd_idattr);
 	relation->rd_idattr = NULL;
-	bms_free(relation->rd_hotblockingattr);
-	relation->rd_hotblockingattr = NULL;
+	bms_free(relation->rd_indexedattr);
+	relation->rd_indexedattr = NULL;
 	bms_free(relation->rd_summarizedattr);
 	relation->rd_summarizedattr = NULL;
 
@@ -5575,7 +5583,7 @@ restart:
 	relation->rd_keyattr = bms_copy(uindexattrs);
 	relation->rd_pkattr = bms_copy(pkindexattrs);
 	relation->rd_idattr = bms_copy(idindexattrs);
-	relation->rd_hotblockingattr = bms_copy(hotblockingattrs);
+	relation->rd_indexedattr = bms_copy(indexedattrs);
 	relation->rd_summarizedattr = bms_copy(summarizedattrs);
 	relation->rd_attrsvalid = true;
 	MemoryContextSwitchTo(oldcxt);
@@ -5589,8 +5597,8 @@ restart:
 			return pkindexattrs;
 		case INDEX_ATTR_BITMAP_IDENTITY_KEY:
 			return idindexattrs;
-		case INDEX_ATTR_BITMAP_HOT_BLOCKING:
-			return hotblockingattrs;
+		case INDEX_ATTR_BITMAP_INDEXED:
+			return indexedattrs;
 		case INDEX_ATTR_BITMAP_SUMMARIZED:
 			return summarizedattrs;
 		default:
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 9e35961fb9e..7a3a0e922dc 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -389,8 +389,8 @@ extern TM_Result heap_update(Relation relation, const ItemPointerData *otid,
 							 HeapTuple newtup,
 							 CommandId cid, uint32 options,
 							 Snapshot crosscheck, bool wait,
-							 TM_FailureData *tmfd, LockTupleMode *lockmode,
-							 TU_UpdateIndexes *update_indexes);
+							 TM_FailureData *tmfd, const LockTupleMode lockmode,
+							 const Bitmapset *modified_idx_attrs, const bool hot_allowed);
 extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
 								 CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy,
 								 bool follow_updates,
@@ -463,6 +463,12 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 									  OffsetNumber *dead, int ndead,
 									  OffsetNumber *unused, int nunused);
 
+/* in heap/heapam.c */
+extern bool HeapUpdateHotAllowable(Relation relation, const Bitmapset *modified_idx_attrs,
+								   bool *summarized_only);
+extern LockTupleMode HeapUpdateDetermineLockmode(Relation relation,
+												 const Bitmapset *modified_idx_attrs);
+
 /* in heap/vacuumlazy.c */
 extern void heap_vacuum_rel(Relation rel,
 							const VacuumParams *params, BufferAccessStrategy bstrategy);
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index ea3f2a6be99..51c812e40dc 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -586,6 +586,7 @@ typedef struct TableAmRoutine
 								 bool wait,
 								 TM_FailureData *tmfd,
 								 LockTupleMode *lockmode,
+								 const Bitmapset *modified_idx_attrs,
 								 TU_UpdateIndexes *update_indexes);
 
 	/* see table_tuple_lock() for reference about parameters */
@@ -1594,6 +1595,10 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid,
  *		during table rewrites where RelationIsLogicallyLogged(rel) is not
  *		yet accurate for the new relation.
  *	crosscheck - if not InvalidSnapshot, also check old tuple against this
+ *	modified_idx_attrs - the indexed attributes whose values differ between
+ *		the old tuple and the new one, offset by
+ *		FirstLowInvalidHeapAttributeNumber; see ExecUpdateModifiedIdxAttrs().
+ *		The caller owns it.
  *
  * Output parameters:
  *	slot - newly constructed tuple data to store
@@ -1621,12 +1626,12 @@ table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot,
 				   CommandId cid, uint32 options,
 				   Snapshot snapshot, Snapshot crosscheck,
 				   bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode,
-				   TU_UpdateIndexes *update_indexes)
+				   const Bitmapset *modified_idx_attrs, TU_UpdateIndexes *update_indexes)
 {
 	return rel->rd_tableam->tuple_update(rel, otid, slot,
 										 cid, options, snapshot, crosscheck,
-										 wait, tmfd,
-										 lockmode, update_indexes);
+										 wait, tmfd, lockmode,
+										 modified_idx_attrs, update_indexes);
 }
 
 /*
@@ -2111,6 +2116,7 @@ extern void simple_table_tuple_delete(Relation rel, ItemPointer tid,
 									  Snapshot snapshot);
 extern void simple_table_tuple_update(Relation rel, ItemPointer otid,
 									  TupleTableSlot *slot, Snapshot snapshot,
+									  const Bitmapset *modified_idx_attrs,
 									  TU_UpdateIndexes *update_indexes);
 
 
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 152a1dfa568..5cbdb092564 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -817,5 +817,8 @@ extern ResultRelInfo *ExecLookupResultRelByOid(ModifyTableState *node,
 											   Oid resultoid,
 											   bool missing_ok,
 											   bool update_cache);
+extern Bitmapset *ExecUpdateModifiedIdxAttrs(ResultRelInfo *relinfo,
+											 TupleTableSlot *old_tts,
+											 TupleTableSlot *new_tts);
 
 #endif							/* EXECUTOR_H  */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 7b8c2b1e362..09025b7e299 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -162,7 +162,7 @@ typedef struct RelationData
 	Bitmapset  *rd_keyattr;		/* cols that can be ref'd by foreign keys */
 	Bitmapset  *rd_pkattr;		/* cols included in primary key */
 	Bitmapset  *rd_idattr;		/* included in replica identity index */
-	Bitmapset  *rd_hotblockingattr; /* cols blocking HOT update */
+	Bitmapset  *rd_indexedattr; /* all cols referenced by indexes */
 	Bitmapset  *rd_summarizedattr;	/* cols indexed by summarizing indexes */
 
 	PublicationDesc *rd_pubdesc;	/* publication descriptor, or NULL */
diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h
index 3e97da6a538..aabd2a44940 100644
--- a/src/include/utils/relcache.h
+++ b/src/include/utils/relcache.h
@@ -70,7 +70,7 @@ typedef enum IndexAttrBitmapKind
 	INDEX_ATTR_BITMAP_KEY,
 	INDEX_ATTR_BITMAP_PRIMARY_KEY,
 	INDEX_ATTR_BITMAP_IDENTITY_KEY,
-	INDEX_ATTR_BITMAP_HOT_BLOCKING,
+	INDEX_ATTR_BITMAP_INDEXED,
 	INDEX_ATTR_BITMAP_SUMMARIZED,
 } IndexAttrBitmapKind;
 
diff --git a/src/test/modules/injection_points/expected/syscache-update-pruned.out b/src/test/modules/injection_points/expected/syscache-update-pruned.out
index a6a4e8db996..07ef67a1eb4 100644
--- a/src/test/modules/injection_points/expected/syscache-update-pruned.out
+++ b/src/test/modules/injection_points/expected/syscache-update-pruned.out
@@ -16,8 +16,8 @@ step wakeinval4:
 step at2: <... completed>
 step wakeinval4: <... completed>
 step wakegrant4: 
-	SELECT FROM injection_points_detach('heap_update-before-pin');
-	SELECT FROM injection_points_wakeup('heap_update-before-pin');
+	SELECT FROM injection_points_detach('simple_heap_update-before-pin');
+	SELECT FROM injection_points_wakeup('simple_heap_update-before-pin');
  <waiting ...>
 step grant1: <... completed>
 ERROR:  tuple concurrently deleted
@@ -42,8 +42,8 @@ step mkrels4:
 	SELECT FROM vactest.mkrels('intruder', 1, 100);  -- repopulate LP_UNUSED
 
 step wakegrant4: 
-	SELECT FROM injection_points_detach('heap_update-before-pin');
-	SELECT FROM injection_points_wakeup('heap_update-before-pin');
+	SELECT FROM injection_points_detach('simple_heap_update-before-pin');
+	SELECT FROM injection_points_wakeup('simple_heap_update-before-pin');
  <waiting ...>
 step grant1: <... completed>
 ERROR:  duplicate key value violates unique constraint "pg_class_oid_index"
@@ -71,8 +71,8 @@ step at2: <... completed>
 step wakeinval4: <... completed>
 step at4: ALTER TABLE vactest.child50 INHERIT vactest.orig50;
 step wakegrant4: 
-	SELECT FROM injection_points_detach('heap_update-before-pin');
-	SELECT FROM injection_points_wakeup('heap_update-before-pin');
+	SELECT FROM injection_points_detach('simple_heap_update-before-pin');
+	SELECT FROM injection_points_wakeup('simple_heap_update-before-pin');
  <waiting ...>
 step grant1: <... completed>
 step wakegrant4: <... completed>
diff --git a/src/test/modules/injection_points/specs/syscache-update-pruned.spec b/src/test/modules/injection_points/specs/syscache-update-pruned.spec
index e3a4295bd12..fef9ac895a1 100644
--- a/src/test/modules/injection_points/specs/syscache-update-pruned.spec
+++ b/src/test/modules/injection_points/specs/syscache-update-pruned.spec
@@ -103,7 +103,7 @@ session s1
 setup	{
 	SET debug_discard_caches = 0;
 	SELECT FROM injection_points_set_local();
-	SELECT FROM injection_points_attach('heap_update-before-pin', 'wait');
+	SELECT FROM injection_points_attach('simple_heap_update-before-pin', 'wait');
 }
 step cachefill1	{ SELECT FROM vactest.reloid_catcache_set('vactest.orig50'); }
 step grant1	{ GRANT SELECT ON vactest.orig50 TO PUBLIC; }
@@ -140,8 +140,8 @@ step mkrels4	{
 	SELECT FROM vactest.mkrels('intruder', 1, 100);  -- repopulate LP_UNUSED
 }
 step wakegrant4	{
-	SELECT FROM injection_points_detach('heap_update-before-pin');
-	SELECT FROM injection_points_wakeup('heap_update-before-pin');
+	SELECT FROM injection_points_detach('simple_heap_update-before-pin');
+	SELECT FROM injection_points_wakeup('simple_heap_update-before-pin');
 }
 step at4	{ ALTER TABLE vactest.child50 INHERIT vactest.orig50; }
 step wakeinval4	{
-- 
2.50.1

