From 9d198cb55b85009aca42ee3c0625a29d18f0fbb5 Mon Sep 17 00:00:00 2001 From: Ethan Mertz Date: Tue, 16 Jun 2026 14:07:01 +0000 Subject: [PATCH v4] Improve index selection for REPLICA IDENTITY FULL When multiple usable indexes exist for a relation with REPLICA IDENTITY FULL, the subscriber now prefers a unique index over a non-unique one. A unique index guarantees at most one tuple per index scan, avoiding the need to scan extra tuples when multiple rows match the lookup key. For non-unique indexes, the existing behavior is preserved: the first usable index found is returned, since index selectivity cannot be reasoned about without invoking the planner. Previously, the first eligible index found was returned without considering whether a unique index existed. --- src/backend/replication/logical/relation.c | 19 ++++- .../subscription/t/032_subscribe_use_index.pl | 71 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c index 296cbaede30..927f6176493 100644 --- a/src/backend/replication/logical/relation.c +++ b/src/backend/replication/logical/relation.c @@ -789,22 +789,35 @@ static Oid FindUsableIndexForReplicaIdentityFull(Relation localrel, AttrMap *attrmap) { List *idxlist = RelationGetIndexList(localrel); + Oid chosen_idx = InvalidOid; foreach_oid(idxoid, idxlist) { bool isUsableIdx; + bool is_unique; Relation idxRel; idxRel = index_open(idxoid, AccessShareLock); isUsableIdx = IsIndexUsableForReplicaIdentityFull(idxRel, attrmap); + is_unique = idxRel->rd_index->indisunique; index_close(idxRel, AccessShareLock); - /* Return the first eligible index found */ - if (isUsableIdx) + if (!isUsableIdx) + continue; + + /* + * A unique index guarantees at most one tuple per index scan. Return + * it immediately rather than continuing to look for a better + * candidate. For non-unique indexes we keep scanning, accepting the + * last usable one found. + */ + if (is_unique) return idxoid; + + chosen_idx = idxoid; } - return InvalidOid; + return chosen_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..657fd52617d 100644 --- a/src/test/subscription/t/032_subscribe_use_index.pl +++ b/src/test/subscription/t/032_subscribe_use_index.pl @@ -547,6 +547,77 @@ $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 over non-unique ones +# + +# 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 and a unique index; the unique one 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); + +# capture idx_scan before the update +$node_subscriber->safe_psql('postgres', "SELECT pg_stat_force_next_flush()"); +my $idx_scan_before = $node_subscriber->safe_psql('postgres', + "SELECT idx_scan FROM pg_stat_all_indexes WHERE indexrelname = 'test_idx_select_uniq'"); + +# 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 unique index is used on the subscriber +$node_publisher->wait_for_catchup($appname); +$node_subscriber->poll_query_until('postgres', + qq{SELECT idx_scan > $idx_scan_before FROM pg_stat_all_indexes, pg_stat_force_next_flush() 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 +$node_subscriber->safe_psql('postgres', "SELECT pg_stat_force_next_flush()"); +$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 over non-unique ones +# ============================================================================= + $node_subscriber->stop('fast'); $node_publisher->stop('fast'); -- 2.47.3