Logical replication in PostgreSQL got a lot of features and performance improvements over the last releases. It was introduced in PostgreSQL 10 back in 2017, and PostgreSQL 9.6 (in 2016) introduced logical decoding which is the basis for logical replication. Today logical replication is really mature and from my point of view only two major features are missing: DDL replication and the replication of sequences. The latter is now possible with the upcoming PostgreSQL 19 next year, and this is what this post is about.
Before we can see how this works we need a logical replication setup. An easy method to set this up is to create a physical replica and then transform that into a logical replica using pg_createsubscriber:
postgres@:/home/postgres/ [pgdev] psql -c "create table t ( a int primary key generated always as identity, b text)"
CREATE TABLE
postgres@:/home/postgres/ [pgdev] psql -c "insert into t (b) values ('aaaa')"
INSERT 0 1
postgres@:/home/postgres/ [pgdev] psql -c "insert into t (b) values ('bbbb')"
INSERT 0 1
postgres@:/home/postgres/ [pgdev] pg_basebackup --pgdata=/var/tmp/dummy --write-recovery-conf --checkpoint=fast
postgres@:/home/postgres/ [pgdev] echo "port=8888" >> /var/tmp/dummy/postgresql.auto.conf
postgres@:/home/postgres/ [pgdev] pg_createsubscriber --all --pgdata=/var/tmp/dummy --subscriber-port=8888 --publisher-server="host=localhost,port=5432"
postgres@:/home/postgres/ [pgdev] pg_ctl --pgdata=/var/tmp/dummy start
2025-11-11 13:21:20.818 CET - 1 - 9669 - - @ - 0LOG: redirecting log output to logging collector process
2025-11-11 13:21:20.818 CET - 2 - 9669 - - @ - 0HINT: Future log output will appear in directory "pg_log".
2025-11-11 13:21:21.250 CET - 1 - 9684 - - @ - 0LOG: redirecting log output to logging collector process
2025-11-11 13:21:21.250 CET - 2 - 9684 - - @ - 0HINT: Future log output will appear in directory "pg_log".
Once this is done we have a logical replica and the data is synchronized:
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "select * from t"
a | b
---+------
1 | aaaa
2 | bbbb
(2 rows)
A quick check the replication is ongoing:
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "insert into t (b) values('cccc');"
INSERT 0 1
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "select * from t"
a | b
---+------
1 | aaaa
2 | bbbb
3 | cccc
(3 rows)
The “generated always as identidy” we used above to create the table automatically created a sequence for us:
ostgres@:/home/postgres/ [pgdev] psql -p 5432 -c "\x" -c "select * from pg_sequences;"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 3
Checking the same sequence on the replica clearly shows that the sequence is not synchronized (last_value is still a 2):
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "\x" -c "select * from pg_sequences;"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 2
The reason is, that sequences are not synchronized automatically:
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "\x" -c "select * from pg_publication;"
Expanded display is on.
-[ RECORD 1 ]---+-------------------------------
oid | 16397
pubname | pg_createsubscriber_5_f58e9acd
pubowner | 10
puballtables | t
puballsequences | f
pubinsert | t
pubupdate | t
pubdelete | t
pubtruncate | t
pubviaroot | f
pubgencols | n
As there currently is no way to enable sequence synchronization for an existing publication we can either drop and re-create or add an additional publication and subscription just for the sequences:
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "create publication pubseq for all sequences;"
CREATE PUBLICATION
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "\x" -c "select * from pg_publication;"
Expanded display is on.
-[ RECORD 1 ]---+-------------------------------
oid | 16397
pubname | pg_createsubscriber_5_f58e9acd
pubowner | 10
puballtables | t
puballsequences | f
pubinsert | t
pubupdate | t
pubdelete | t
pubtruncate | t
pubviaroot | f
pubgencols | n
-[ RECORD 2 ]---+-------------------------------
oid | 16398
pubname | pubseq
pubowner | 10
puballtables | f
puballsequences | t
pubinsert | t
pubupdate | t
pubdelete | t
pubtruncate | t
pubviaroot | f
pubgencols | n
The publication has sequence replication enabled and the subscription to consume this can be created like this:
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "create subscription subseq connection 'host=localhost port=5432' publication pubseq"
CREATE SUBSCRIPTION
Now the sequence is visible in pg_subscription_rel on the subscriber:
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "select * from pg_subscription_rel;"
srsubid | srrelid | srsubstate | srsublsn
---------+---------+------------+------------
24589 | 16385 | r |
24590 | 16384 | r | 0/04004780 -- sequence
(2 rows)
State “r” means ready, so the sequence should have synchronized, and indeed:
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "\x" -c "select * from pg_sequences"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 3
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "\x" -c "select * from pg_sequences"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 3
Adding new rows to the table, which also increases the last_value of the sequence, should also synchronize the sequences:
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "insert into t (b) values ('eeee')"
INSERT 0 1
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "insert into t (b) values ('ffff')"
INSERT 0 1
postgres@:/home/postgres/ [pgdev] psql -p 5432 -c "\x" -c "select * from pg_sequences"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 6
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "\x" -c "select * from pg_sequences"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 4
… but is not happening automatically. To get them synchronized you need to refresh the subscription:
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "\x" -c "alter subscription subseq refresh sequences"
Expanded display is on.
ALTER SUBSCRIPTION
postgres@:/home/postgres/ [pgdev] psql -p 8888 -c "\x" -c "select * from pg_sequences"
Expanded display is on.
-[ RECORD 1 ]-+-----------
schemaname | public
sequencename | t_a_seq
sequenceowner | postgres
data_type | integer
start_value | 1
min_value | 1
max_value | 2147483647
increment_by | 1
cycle | f
cache_size | 1
last_value | 6
Great, this reduces the work to fix the sequences quite a bit and is really helpful. As usual, thanks to all involved.