Welcome back to the PG19 Hacktober series!!
PostgreSQL 19 is on the way with new enhancements, including notable improvements to logical replication. In this blog, we will explore how sequences can be replicated from the publisher to the subscriber using logical sequence replication. We will also discuss the performance improvements, how sequence replication works behind the scenes, and how it worked in previous PostgreSQL versions, such as PostgreSQL 18.
Background / Context
- We are aware that in PostgreSQL 18, sequences are not logically replicated as part of
native tableorDDL logical replication.
- The sequence capture happens in
PostgreSQL 18when a row is inserted into a table that uses an auto-increment column called serial or Identity, PostgreSQL reads the sequence object, assigns the next ID, and commits the row.
- Here, in PostgreSQL 18, logical replication streams the actual row data containing that new ID, but it does not stream the internal counter or state of the sequence object itself.
Pain points
- Primary key conflicts during failover: In PostgreSQL 18, logical replication copies table data to the subscriber, but it does not automatically replicate sequence values. If the subscriber is promoted to primary during failover, its sequence may generate an ID that already exists in the table, causing a duplicate key error.
- Manual sequence synchronization is risky: We often use scripts to find the maximum ID in the publisher table and update the subscriber’s sequence using setval(). However, if a new row is inserted on the publisher between these two steps, the sequence may become outdated, leading to duplicate IDs and insert failures.
- Standalone sequences can be missed: Custom scripts usually focus on sequences associated with SERIAL or IDENTITY columns. Standalone sequences, which are created manually and called directly by application logic, can easily be overlooked and remain out of sync.
Why was it so hard to implement?
Why Did Sequence Replication Take So Long?
PostgreSQL introduced native logical replication in PostgreSQL 10, but sequence replication remained a challenge for many years. The main reason was the difference between how transactions work and how sequences are designed for performance.
1. Sequences Do Not Follow Transaction Rollbacks
Logical replication reads changes from the Write-Ahead Log (WAL) and applies transactional changes in commit order.
Sequences work differently. They are designed to generate numbers quickly without waiting for transactions to finish.
For example:
- Transaction A gets sequence value 1.
- Transaction B gets sequence value 2.
- Transaction A fails and rolls back.
Even though Transaction A failed, sequence value 1 is not returned to the sequence. The next value continues from 3.
This behavior helps PostgreSQL avoid unnecessary locking and allows multiple transactions to generate sequence values concurrently.
Because sequence values do not roll back with transactions, replicating them correctly through logical replication is more complicated.
2. Replicating Every Sequence Change Can Affect Performance
Imagine a database processing thousands of inserts every second. Each insert may call nextval() to generate a new ID.
If PostgreSQL recorded and replicated every sequence increment, it would create additional replication traffic and processing overhead.
This could affect performance, especially in databases with high transaction volumes.
Therefore, PostgreSQL needed an approach that could replicate sequence state correctly without adding too much overhead.
An earlier implementation was attempted during PostgreSQL 16 development but was later reverted because of the complexity involved in handling sequence changes safely and efficiently.
What’s New in PostgreSQL 19:
PostgreSQL 19 takes a simpler approach. Instead of replicating every sequence increment continuously, it synchronizes sequence values at specific points in time.
Sequence synchronization happens during these operations:
CREATE SUBSCRIPTION
ALTER SUBSCRIPTION ... REFRESH PUBLICATION
ALTER SUBSCRIPTION ... REFRESH SEQUENCES
Sequence Sync Workers: PostgreSQL 19 Use background workers to synchronize sequence values instead of replicating every nextval() increment individually.
Synchronization at Specific Events: Sequence values are synchronized when creating a subscription, refreshing a publication, or running ALTER SUBSCRIPTION … REFRESH SEQUENCES.
Native Publication Support: PostgreSQL 19 introduces FOR ALL SEQUENCES and FOR ALL TABLES, ALL SEQUENCES options, helping include standalone sequences in logical replication.
This means PostgreSQL can copy sequence state without sending every nextval() operation through the logical replication stream.
Hands-On Example:
Step1: Create publications for the table(s) as well as the sequence(s) and create a subscription.
Create a table and create a sequence on any column by using SERIAL and check the sequence associated with that column along with sequence name.
postgres=# CREATE TABLE public.logical_sequence (
id SERIAL PRIMARY KEY,
employee_name TEXT NOT NULL,
department TEXT
);
CREATE TABLE
SELECT pg_get_serial_sequence(
'public.logical_sequence', 'id'
);
pg_get_serial_sequence
--------------------------------
public.logical_sequence_id_seq
postgres=# CREATE PUBLICATION pagila_pub FOR TABLE public.logical_sequence;
CREATE PUBLICATION
postgres=# CREATE PUBLICATION pagila_seq_pub FOR ALL SEQUENCES;
CREATE PUBLICATION
Step 2:Check the publications
postgres=# SELECT pubname FROM pg_publication
WHERE pubname IN ('pagila_pub', 'pagila_seq_pub');
pubname
----------------
pagila_pub
pagila_seq_pub
Step3: Create a subscription.
pagila=# CREATE SUBSCRIPTION pagila_se
CONNECTION 'host=192.168.56.12 port=5434 dbname=pagila user=repl_user2 password=mydb'
PUBLICATION pagila_pub, pagila_seq_pub;
NOTICE: created replication slot "pagila_se" on publisher
CREATE SUBSCRIPTION
Subscribe the above to pagila_pub & pagila_seq_pub
pagila=# SELECT
pubname AS publication_name,
schemaname,
sequencename
FROM pg_publication_sequences
WHERE schemaname = 'public'
AND sequencename = 'logical_sequence_id_seq';
publication_name | schemaname | sequencename
------------------+------------+-------------------------
pagila_seq_pub | public | logical_sequence_id_seq
The logical sequence_id belongs to the publication of ” pagila_seq_pub “
Step 4: Check the sequence synchronization as well as the values being replicated from the primary.
Primary:
pagila=# INSERT INTO public.logical_sequence
(employee_name, department)
SELECT
'Employee_' || g,
'Testing'
FROM generate_series(1, 40) AS g;
INSERT 0 40
pagila=# SELECT COUNT(*) AS row_count,
MIN(id) AS min_id,
MAX(id) AS max_id
FROM public.logical_sequence;
row_count | min_id | max_id
-----------+--------+--------
43 | 1 | 43
(1 row)
Subscriber:
Before checking the sequences and values, check the subscriber
pagila=# SELECT
srrelid::regclass AS object_name,
srsubstate,
srsublsn
FROM pg_subscription_rel
WHERE srsubid = (
SELECT oid
FROM pg_subscription
WHERE subname = 'pagila_se'
)
ORDER BY srrelid::regclass::text;
object_name | srsubstate | srsublsn
-------------------------+------------+-------------
logical_sequence | r | 11/6A8EEC18
logical_sequence_id_seq | r | 11/6A8A71A0
(2 rows)
postgres=# SELECT COUNT(*) AS row_count,
MIN(id) AS min_id,
MAX(id) AS max_id
FROM public.logical_sequence;
row_count | min_id | max_id
-----------+--------+--------
43 | 1 | 43
(1 row)
pagila=# SELECT pubname, puballsequences, puballtables
FROM pg_publication
WHERE pubname IN ('pagila_pub', 'pagila_seq_pub');
pubname | puballsequences | puballtables
----------------+-----------------+--------------
pagila_pub | f | f
pagila_seq_pub | t | f
Here, we can observe that when a publication is created, sequences are also replicated from the publication to the subscription.
Primary:
check the sequences which are assiciated to your publication on publication/primary node ,here I have associated my sequence called “logical_sequence_id_seq” to “pagila_seq_pub” publication.
postgres=#SELECT *
FROM pg_publication_sequences
WHERE pubname = 'pagila_seq_pub';
pubname | schemaname | sequencename
----------------+------------+-------------------------
pagila_seq_pub | public | logical_sequence_id_seq
postgres=# SELECT nextval('public.logical_sequence_id_seq')
FROM generate_series(1, 70);
generated_value
-----------------
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411...........443
postgres=# SELECT nextval('public.logical_sequence_id_seq') AS generated_value
FROM generate_series(1, 70);
pagila=# SELECT *
FROM pg_get_sequence_data('public.logical_sequence_id_seq');
last_value | is_called | page_lsn
------------+-----------+-------------
443 | t | 11/6AB1E540
Subscriber: check synchronization
postgres=# SELECT last_value, is_called
FROM public.logical_sequence_id_seq;
last_value | is_called
------------+-----------
303 | t
(1 row)
postgres=# SELECT srrelid::regclass AS object_name,
srsubstate,
srsublsn
FROM pg_subscription_rel
WHERE srsubid = (
SELECT oid
FROM pg_subscription
WHERE subname = 'pagila_se'
)
AND srrelid = 'public.logical_sequence_id_seq'::regclass;
object_name | srsubstate | srsublsn
-------------------------+------------+-------------
logical_sequence_id_seq | r | 11/6A8A71A0
(1 row)
The subscriber values are not in sync,so refresh by using Alter command.
postgres=# ALTER SUBSCRIPTION pagila_se REFRESH SEQUENCES;
ALTER SUBSCRIPTION
pagila=# SELECT last_value, is_called
FROM public.logical_sequence_id_seq;
last_value | is_called
------------+-----------
443 | t
(1 row)
After refreshing on the subscriber, the values are in sync.
Why It Matters:
| No | Use case | Real-world example | Benefit |
| 1 | Major version upgrades | Upgrading PostgreSQL 14 to PostgreSQL 19 using logical replication. | Synchronizes sequence values before cutover and helps prevent duplicate IDs. |
| 2 | On-premises to cloud migration | Moving a PostgreSQL database from a local data center to Azure or AWS. | Helps the target database continue generating IDs correctly after migration. |
| 3 | Disaster recovery | Promoting a logical subscriber after the primary server fails. | Reduces the risk of primary key conflicts on the new primary. |
| 4 | Blue-green deployment | Running old and new database environments side by side before switching applications. | Helps synchronize sequence state before switching to the new environment. |
| 5 | Data center migration | Moving a production database from one data center to another. | Simplifies sequence synchronization during the final cutover. |
| 6 | Staging and QA environments | Replicating production data into a testing database. | Helps prevent duplicate IDs when developers test inserts and application workflows. |
| 7 | Database platform migration | Migrating a database between PostgreSQL-compatible platforms using logical replication. | Reduces the need for custom scripts to synchronize sequence values. |
| 8 | Planned switchover | Moving production workloads from the current primary to a new server during maintenance. | Allows administrators to refresh sequence values before promoting the subscriber. |
Caveats / Things to Watch Out For
The main limitation for logical sequence replication is that sequence synchronization is not continuous. If the publisher generates more values after synchronization, the subscriber’s sequence can become outdated again. Therefore, synchronize sequences immediately before promoting the subscriber during a planned failover or switchover.
When to Refresh Sequences PostgreSQL
- Before a planned failover to a logical replica.
- Before promoting a subscriber to the primary server.
- After completing a major PostgreSQL version upgrade.
- After large bulk inserts on the publisher.
- On a scheduled cron job for hot standbys (every N minutes).
- Never refresh sequences after every individual
INSERT, table replication handles row changes, but sequence state is not automatically synchronized through standard logical replication.
Conclusion:
PostgreSQL 19 introduces native sequence synchronization in logical replication, reducing the need for custom scripts and helping prevent duplicate primary key errors during migrations and failovers. Sequence values can be refreshed using subscription commands without additional server configuration parameters. However, synchronization is not continuous, so sequences should be refreshed before cutover or failover.
References:
Looking Forward to Postgres 19: Logically Sequenced
https://www.postgresql.org/docs/19/logical-replication-sequences.html
.
