From c507e655566b74883b80c42fda2750bf169952ba Mon Sep 17 00:00:00 2001 From: Ethan Mertz Date: Thu, 21 May 2026 22:41:13 +0000 Subject: [PATCH v2] Improve index selection for REPLICA IDENTITY FULL When multiple usable indexes exist for a relation with REPLICA IDENTITY FULL, the subscriber now prefers unique indexes over non-unique ones (since a unique index guarantees at most one tuple per index scan), and among unique indexes, prefers those with fewer key columns. For non-unique indexes, the first usable one found is accepted without further ranking, since more key columns can narrow the search space and fewer columns is not necessarily better. Previously, the first eligible index found was returned without considering whether a better candidate existed. --- src/backend/replication/logical/relation.c | 46 ++++++++++--- .../subscription/t/032_subscribe_use_index.pl | 66 +++++++++++++++++++ 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c index 0b1d80b5b0f..47538977ffa 100644 --- a/src/backend/replication/logical/relation.c +++ b/src/backend/replication/logical/relation.c @@ -784,28 +784,54 @@ logicalrep_partition_open(LogicalRepRelMapEntry *root, * We expect to call this function when REPLICA IDENTITY FULL is defined for * the remote relation. * + * If multiple usable indexes exist, unique indexes are preferred (they + * guarantee at most one tuple per scan), and among unique indexes those with + * fewer key columns win. The first usable non-unique index is accepted + * without further ranking. + * * If no suitable index is found, returns InvalidOid. */ static Oid FindUsableIndexForReplicaIdentityFull(Relation localrel, AttrMap *attrmap) { List *idxlist = RelationGetIndexList(localrel); + Oid best_idx = InvalidOid; + bool best_is_unique = false; + int best_nkeyatts = PG_INT32_MAX; foreach_oid(idxoid, idxlist) { - bool isUsableIdx; - Relation idxRel; - - idxRel = index_open(idxoid, AccessShareLock); - isUsableIdx = IsIndexUsableForReplicaIdentityFull(idxRel, attrmap); - index_close(idxRel, AccessShareLock); + bool is_usable; + bool is_unique; + int nkeyatts; + Relation idxrel; + + idxrel = index_open(idxoid, AccessShareLock); + is_usable = IsIndexUsableForReplicaIdentityFull(idxrel, attrmap); + is_unique = idxrel->rd_index->indisunique; + nkeyatts = idxrel->rd_index->indnkeyatts; + index_close(idxrel, AccessShareLock); + + if (!is_usable) + continue; - /* Return the first eligible index found */ - if (isUsableIdx) - return idxoid; + /* + * Prefer unique indexes (at most one index scan per tuple match), and + * among those prefer fewer key columns. For non-unique indexes we + * just accept the first one found, since more keys can narrow the + * search space and fewer columns is not necessarily better. + */ + if (best_idx == InvalidOid || + (is_unique && !best_is_unique) || + (best_is_unique && is_unique && nkeyatts < best_nkeyatts)) + { + best_idx = idxoid; + best_is_unique = is_unique; + best_nkeyatts = nkeyatts; + } } - return InvalidOid; + return best_idx; } /* diff --git a/src/test/subscription/t/032_subscribe_use_index.pl b/src/test/subscription/t/032_subscribe_use_index.pl index c755c1a7518..1ca23f96f5e 100644 --- a/src/test/subscription/t/032_subscribe_use_index.pl +++ b/src/test/subscription/t/032_subscribe_use_index.pl @@ -547,6 +547,72 @@ $node_subscriber->safe_psql('postgres', "DROP TABLE test_replica_id_full"); # Testcase end: Subscription can use hash index # ============================================================================= +# ============================================================================= +# Testcase start: Index selection prefers unique indexes and fewer key columns +# + +# create tables pub and sub +$node_publisher->safe_psql('postgres', + "CREATE TABLE test_idx_select (a int, b int, c int)"); +$node_publisher->safe_psql('postgres', + "ALTER TABLE test_idx_select REPLICA IDENTITY FULL"); +$node_subscriber->safe_psql('postgres', + "CREATE TABLE test_idx_select (a int, b int, c int)"); + +# create a non-unique index on (a, b, c) and a unique index on (a, b) +# the unique index with fewer columns should be preferred +$node_subscriber->safe_psql('postgres', + "CREATE INDEX test_idx_select_nonuniq ON test_idx_select(a, b, c)"); +$node_subscriber->safe_psql('postgres', + "CREATE UNIQUE INDEX test_idx_select_uniq ON test_idx_select(a, b)"); + +# create pub/sub +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION tap_pub_idx_select FOR TABLE test_idx_select"); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub_idx_select CONNECTION '$publisher_connstr application_name=$appname' PUBLICATION tap_pub_idx_select" +); + +# wait for initial table synchronization to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, $appname); + +# insert and update a row +$node_publisher->safe_psql('postgres', + "INSERT INTO test_idx_select VALUES (1, 2, 3)"); +$node_publisher->safe_psql('postgres', + "UPDATE test_idx_select SET c = 4 WHERE a = 1"); + +# wait until the index is used on the subscriber +$node_publisher->wait_for_catchup($appname); +$node_subscriber->poll_query_until('postgres', + q{select (idx_scan = 1) from pg_stat_all_indexes where indexrelname = 'test_idx_select_uniq';} + ) + or die + "Timed out while waiting for check subscriber tap_sub_idx_select updates one row via unique index"; + +# make sure that the non-unique index was not used +$result = $node_subscriber->safe_psql('postgres', + "select idx_scan from pg_stat_all_indexes where indexrelname = 'test_idx_select_nonuniq'"); +is($result, qq(0), + 'ensure non-unique index is not used when unique index is available'); + +# make sure that the subscriber has the correct data +$result = $node_subscriber->safe_psql('postgres', + "select c from test_idx_select where a = 1"); +is($result, qq(4), + 'ensure subscriber has the correct data at the end of the test'); + +# cleanup pub +$node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_idx_select"); +$node_publisher->safe_psql('postgres', "DROP TABLE test_idx_select"); + +# cleanup sub +$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_idx_select"); +$node_subscriber->safe_psql('postgres', "DROP TABLE test_idx_select"); + +# Testcase end: Index selection prefers unique indexes and fewer key columns +# ============================================================================= + $node_subscriber->stop('fast'); $node_publisher->stop('fast'); -- 2.47.3