From 03f631e2a597c5694cbf218fe74877e73c6c88bc Mon Sep 17 00:00:00 2001 From: Nisha Moond Date: Wed, 30 Sep 2026 15:33:03 +0530 Subject: [PATCH v4 1/2] Use the relation map's index when searching deleted tuples sequentially When the index can't be used to find a recently deleted row, RelationFindDeletedTupleInfoSeq() scans the table. It looked up the relation's replica identity or primary key again to decide which columns to compare, which could disagree with the index chosen when the relation was opened. A concurrent DROP INDEX CONCURRENTLY could remove the replica identity mid-change, so the whole row was compared, and update_deleted was reported as update_missing. Pass the index from the relation map and compare its key columns only when it is the replica identity or primary key. Otherwise compare the whole row, as the publisher then uses REPLICA IDENTITY FULL. Author: Hayato Kuroda Discussion: https://postgr.es/m/CABdArM5ydwdRrpaZyK1q2p3-vY_+pnBtTmkvg_pcM=gHwmH7Kg@mail.gmail.com Backpatch-through: 19 --- src/backend/executor/execReplication.c | 47 ++++++++++++++++-------- src/backend/replication/logical/worker.c | 16 +++++++- src/include/executor/executor.h | 2 +- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index dd42acc13e2..d54f08fda8f 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -561,9 +561,14 @@ update_most_recent_deletion_info(TupleTableSlot *scanslot, * * The commit timestamp of the deleting transaction is used to determine which * tuple was deleted most recently. + * + * If 'idxoid' is valid, its key columns are used for comparison. The index + * must be an identity or primary key index. Otherwise, all columns are used + * for comparison. */ bool -RelationFindDeletedTupleInfoSeq(Relation rel, TupleTableSlot *searchslot, +RelationFindDeletedTupleInfoSeq(Relation rel, Oid idxoid, + TupleTableSlot *searchslot, TransactionId oldestxmin, TransactionId *delete_xid, ReplOriginId *delete_origin, @@ -572,7 +577,7 @@ RelationFindDeletedTupleInfoSeq(Relation rel, TupleTableSlot *searchslot, TupleTableSlot *scanslot; TableScanDesc scan; TypeCacheEntry **eq; - Bitmapset *indexbitmap; + Bitmapset *indexbitmap = NULL; TupleDesc desc PG_USED_FOR_ASSERTS_ONLY = RelationGetDescr(rel); Assert(equalTupleDescs(desc, searchslot->tts_tupleDescriptor)); @@ -582,21 +587,32 @@ RelationFindDeletedTupleInfoSeq(Relation rel, TupleTableSlot *searchslot, *delete_time = 0; /* - * If the relation has a replica identity key or a primary key that is - * unusable for locating deleted tuples (see - * IsIndexUsableForFindingDeletedTuple), a full table scan becomes - * necessary. In such cases, comparing the entire tuple is not required, - * since the remote tuple might not include all column values. Instead, - * the indexed columns alone are sufficient to identify the target tuple - * (see logicalrep_rel_mark_updatable). + * We get here when the caller's index, if any, cannot be used for + * locating deleted tuples (see IsIndexUsableForFindingDeletedTuple). If + * that index is the replica identity or primary key, the remote tuple + * might not include all column values, but the index's key columns alone + * are sufficient to identify the target tuple. Otherwise, the remote + * relation has REPLICA IDENTITY FULL, so compare the entire tuple. */ - indexbitmap = RelationGetIndexAttrBitmap(rel, - INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (OidIsValid(idxoid)) + { + Relation idxrel = index_open(idxoid, AccessShareLock); + + Assert(idxrel->rd_index->indisunique); + Assert(heap_attisnull(idxrel->rd_indextuple, Anum_pg_index_indpred, + NULL)); + + for (int i = 0; i < idxrel->rd_index->indnkeyatts; i++) + { + AttrNumber attnum = idxrel->rd_index->indkey.values[i]; + + Assert(AttributeNumberIsValid(attnum)); + indexbitmap = bms_add_member(indexbitmap, + attnum - FirstLowInvalidHeapAttributeNumber); + } - /* fallback to PK if no replica identity */ - if (!indexbitmap) - indexbitmap = RelationGetIndexAttrBitmap(rel, - INDEX_ATTR_BITMAP_PRIMARY_KEY); + index_close(idxrel, AccessShareLock); + } eq = palloc0_array(TypeCacheEntry *, searchslot->tts_tupleDescriptor->natts); @@ -624,6 +640,7 @@ RelationFindDeletedTupleInfoSeq(Relation rel, TupleTableSlot *searchslot, table_endscan(scan); ExecDropSingleTupleTableSlot(scanslot); + bms_free(indexbitmap); return *delete_time != 0; } diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index ab7c4ced66d..aad14ca7c3c 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -3409,9 +3409,23 @@ FindDeletedTupleInLocalRel(Relation localrel, delete_xid, delete_origin, delete_time); else - return RelationFindDeletedTupleInfoSeq(localrel, remoteslot, + { + /* + * Pass the index only if it is the replica identity or primary key, + * so that its key columns are compared. Use the relation map's + * choice rather than looking it up again, since concurrent DDL may + * have changed the relation's replica identity. + */ + Oid idxoid = relmapentry->idxisreplident ? localidxoid : InvalidOid; + + /* Without such an index, every column is compared. */ + Assert(relmapentry->idxisreplident || + relmapentry->remoterel.replident == REPLICA_IDENTITY_FULL); + + return RelationFindDeletedTupleInfoSeq(localrel, idxoid, remoteslot, oldestxmin, delete_xid, delete_origin, delete_time); + } } /* diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 23a09a70aa2..1e4c51ab7b2 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -781,7 +781,7 @@ extern bool RelationFindReplTupleByIndex(Relation rel, Oid idxoid, TupleTableSlot *outslot); extern bool RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode, TupleTableSlot *searchslot, TupleTableSlot *outslot); -extern bool RelationFindDeletedTupleInfoSeq(Relation rel, +extern bool RelationFindDeletedTupleInfoSeq(Relation rel, Oid idxoid, TupleTableSlot *searchslot, TransactionId oldestxmin, TransactionId *delete_xid, -- 2.54.0 (Apple Git-157)