Pingu
Computer MySQL PostgreSQL Books Publications
Spielereien Kanu Geopolitik Business TopoDB POI Klettersteigen History TransPool Thermal Baden Brokenstuben Goldwaschen
Blog Contact
Shinguz
Google
/ch/open

PostgreSQL Logical Replication

/ home / computer / postgresql / postgresql for mysql admins / .

Table of Contents

Details
  1. Concept
  2. Setting up the Publisher
    • Grant privileges on Publisher
      • Grant privileges for whole instance
      • Grant privileges for one whole database
      • Grant privileges for one whole schema in one database
      • Grant privileges for several tables in one database
    • Publish tables
      • Publish all tables in the database
      • Publish all tables in a schema
      • Publish some tables
    • Copy table structures
      • Copy all tables in the database to Subscriber
      • Copy all tables in a schema to Subscriber
      • Copy some tables to Subscriber
  3. Setting up a Subscriber
  4. Operations
  5. Monitoring
  6. Limitations
  7. Fail-over
  8. Advanced topics
    • Replication filtering

Concept

Logical Replication uses a:

  • Publish and Subscribe model.
  • One or more subscribers, subscribing to one or more publications on a publisher node.
  • Subscribers pull data for the publications they subscribe to.
  • Cascading replication or more complex configurations are possible.

Use-cases

  • Sending incremental changes in a single database or a subset of a database to subscribers as they occur.
  • Firing triggers for individual changes as they arrive on the subscriber.
  • Consolidating multiple databases into a single one.
  • Replicating between different major versions.
  • Replicating between different platforms (Linxu to Windows, 32-bit to 64-bit, little-endian to big-endian).
  • Giving access to replicated data to different groups of users.
  • Sharing a subset of the database between multiple databases. (multi-source?)

Publisher / Publication

  • The node where a publication is defined is referred to as publisher (change set or replication set).
  • Each publication exists in only one database.
  • Every publication can have multiple subscribers.
  • A published table must have a replica identity configured. By default, this is the primary key. Another unique index can also be set to be the replica identity.
    TODO: How to find without replica identity, how to find tables without a pk?
  • If the table does not have any suitable key: replica identiy FULL. Can lead to Full-Table-Scan? on the Subscriber side. Only as a fallback if no other solution is possible.

Subscriber / Subscription

  • The node where a subscription is defined is referred to as the subscriber.
  • A subscriber node may have multiple subscriptions if desired.
  • It is possible to define multiple subscriptions between a single publisher-subscriber pair.
  • Each subscription will receive changes via one replication slot.
  • A logical replication subscription can be a standby for synchronous replication.
  • Subscriptions are dumped by pg_dump.

Setting up the Publisher

Required settings for logical replication on Publisher:

#
# postgresql.conf or zzz-mycompany.conf
#
listen_addresses      = '*'
wal_level             = logical
max_replication_slots = 10   # default
max_wal_senders       = 10   # default

A database instance restart is required here.

postgres=# show wal_level;
 wal_level 
-----------
 logical

Create role:

postgres=# CREATE ROLE replication WITH REPLICATION PASSWORD 'secret';
postgres=# ALTER ROLE replication WITH LOGIN;

postgres=# SELECT rolname AS role, rolsuper AS super, rolcanlogin AS login, rolreplication AS replication
  FROM pg_roles
 WHERE rolsuper = true or rolreplication = true
;
     role     | super | login | replication 
--------------+-------+-------+-------------
 postgres     | t     | t     | t
 replication  | f     | t     | t

Adapt pg_hba.conf:

#
# pg_hba.conf
#
# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    all             replication     10.223.125.0/24         scram-sha-256

postgres=# SELECT pg_reload_conf();

postgres=# SELECT user_name AS role, address, netmask, database
  FROM pg_hba_file_rules
 WHERE type != 'local' AND address NOT IN ('127.0.0.1', '::1')
;
     role      |   address    |    netmask    | database 
---------------+--------------+---------------+----------
 {replication} | 10.223.125.0 | 255.255.255.0 | {all}
 {app}         | 10.223.125.0 | 255.255.255.0 | {test}

Publisher granularity

We consider different scenarios here:

  1. Replicate the whole instance
  2. Replicate one whole database
  3. Replicate one whole schema in one database
  4. Replicate serveral tables in one database

Grant privileges on Publisher

TODO: Show all privileges of a row:

Grant privileges for whole database instance

postgres=# SELECT datname FROM pg_database WHERE datistemplate != true;
 datname  
----------
 prod
 qual

postgres=# \c prod
You are now connected to database "prod" as user "postgres".

prod=# SELECT nspname AS schema
  FROM pg_namespace
 WHERE nspname NOT IN ('pg_toast', 'pg_catalog', 'information_schema')
;
 schema  
---------
 erp
 sales
 finance

prod=# GRANT SELECT ON ALL TABLES IN SCHEMA erp TO replication;
prod=# GRANT SELECT ON ALL TABLES IN SCHEMA sales TO replication;
prod=# GRANT SELECT ON ALL TABLES IN SCHEMA finance TO replication;

prod=# \c qual
You are now connected to database "qual" as user "postgres".

qual=# SELECT nspname AS schema
  FROM pg_namespace
 WHERE nspname NOT IN ('pg_toast', 'pg_catalog', 'information_schema')
;
 schema  
---------
 erp
 sales
 finance

qual=# GRANT SELECT ON ALL TABLES IN SCHEMA erp TO replication;
qual=# GRANT SELECT ON ALL TABLES IN SCHEMA sales TO replication;
qual=# GRANT SELECT ON ALL TABLES IN SCHEMA finance TO replication;

Grant privileges for one whole database

prod=# SELECT nspname AS schema
  FROM pg_namespace
 WHERE nspname NOT IN ('pg_toast', 'pg_catalog', 'information_schema')
;
 schema  
---------
 erp
 sales
 finance

prod=# GRANT SELECT ON ALL TABLES IN SCHEMA erp TO replication;
prod=# GRANT SELECT ON ALL TABLES IN SCHEMA sales TO replication;
prod=# GRANT SELECT ON ALL TABLES IN SCHEMA finance TO replication;
-- GRANT pg_read_all_data TO replication;   -- for all schemas in database

Grant privileges for one whole schema in one database

prod=# SELECT nspname AS schema
  FROM pg_namespace
 WHERE nspname NOT IN ('pg_toast', 'pg_catalog', 'information_schema')
;
 schema  
---------
 erp
 sales
 finance

prod=# GRANT SELECT ON ALL TABLES IN SCHEMA erp TO replication;

Grant privileges for several tables in one database

postgres=# \c test

test=# SELECT nspname AS schema
  FROM pg_namespace
 WHERE nspname NOT IN ('pg_toast', 'pg_catalog', 'information_schema')
;
 schema  
---------
 erp
 sales
 finance
 test

test=# SELECT table_catalog, table_schema, table_name, table_type
  FROM information_schema.tables
 WHERE table_type = 'BASE TABLE'
   AND table_schema = 'public'
;
 table_catalog | table_schema | table_name | table_type 
---------------+--------------+------------+------------
 test          | public       | test       | BASE TABLE

test=# GRANT SELECT ON TABLE public.test TO replication;

test=# \dp test
                                 Access privileges
 Schema | Name | Type  |     Access privileges      | Column privileges | Policies 
--------+------+-------+----------------------------+-------------------+----------
 public | test | table | postgres=arwdDxtm/postgres+|                   | 
        |      |       | app=arwdDxtm/postgres     +|                   | 
        |      |       | replication=r/postgres     |                   | 

Publish tables

Caution: a PUBLICATION is always per database! So, if you want to publish serveral databases you have to create one publication per database.

Publish all tables in the database

postgres=# \connect test

test=# CREATE PUBLICATION db_test_publication FOR ALL TABLES;
CREATE PUBLICATION

test=# SELECT * FROM pg_publication;
  oid  |       pubname       | pubowner | puballtables | pubinsert | pubupdate | pubdelete | pubtruncate | pubviaroot | pubgencols 
-------+---------------------+----------+--------------+-----------+-----------+-----------+-------------+------------+------------
 16405 | db_test_publication |       10 | t            | t         | t         | t         | t           | f          | n

Publish all tables in a schema

-- CREATE PUBLICATION my_publication FOR TABLE table1, table2;

Publish several tables in different schemas

postgres=# \connect test

test=# CREATE PUBLICATION test_var FOR TABLE public.test;
CREATE PUBLICATION

test=# SELECT * FROM pg_publication;
  oid  |       pubname       | pubowner | puballtables | pubinsert | pubupdate | pubdelete | pubtruncate | pubviaroot | pubgencols 
-------+---------------------+----------+--------------+-----------+-----------+-----------+-------------+------------+------------
 16405 | db_test_publication |       10 | t            | t         | t         | t         | t           | f          | n

test=# SELECT * FROM pg_publication_tables;
 pubname  | schemaname | tablename |   attnames   | rowfilter 
----------+------------+-----------+--------------+-----------
 test_var | public     | test      | {id,data,ts} | 

Copy table structures

Copy all table structures in the database to Subscriber

$ pg_dump --host=<publisher-host> --user=postgres --schema-only --dbname=test > /tmp/db_test_full_dump.sql

postgres=# drop database test;
DROP DATABASE

postgres=# create database test;
CREATE DATABASE

$ psql --host=<subscriber-host> --user=postgres --dbname=test --file=/tmp/db_test_full_dump.sql

Copy all table structures in a schema to Subscriber

The easiest way to copy a schema to a Subscriber is the following:

$ # pg_dump --user=postgres --schema-only --table=users --schema=test test > /tmp/db_test_schema_test_dump.sql
$ # transfer the file to subscriber
$ psql --command='DROP DATABASE test'
$ psql --command='CREATE DATABASE test'
$ psql --user=postgres --dbname=test --file=/tmp/db_test_schema_test_dump.sql

Copy some table structures to Subscriber

The easiest way to copy a some table structures to a Subscriber is the following:

$ pg_dump --user=postgres --schema-only --table=public.test test > /tmp/db_test_var_dump.sql
$ # transfer the file to subscriber
$ psql --command='DROP DATABASE test'
$ psql --command='CREATE DATABASE test'
$ psql --user=postgres --dbname=test --file=/tmp/db_test_var_dump.sql
...
psql:/tmp/test_var_dump.sql:80: ERROR:  role "app" does not exist
psql:/tmp/test_var_dump.sql:81: ERROR:  role "replication" does not exist
psql:/tmp/test_var_dump.sql:88: ERROR:  role "app" does not exist

Setting up a Subscriber

During creation of a SUBSCRIPTION a snapshot of the tables is created! This can take a while…

postgres=# \connect test

test=# CREATE SUBSCRIPTION db_test_var_subscription
  CONNECTION 'host=10.223.125.57 port=5432 dbname=test user=replication password=secret'
  PUBLICATION test_var
;
NOTICE:  created replication slot "db_test_var_subscription" on publisher
CREATE SUBSCRIPTION

test=# \x
Expanded display is on.

test=# SELECT * FROM pg_subscription;
-[ RECORD 1 ]-------+--------------------------------------------------------------------------
oid                 | 24613
subdbid             | 24597
subskiplsn          | 0/0
subname             | db_test_var_subscription
subowner            | 10
subenabled          | t
subbinary           | f
substream           | p
subtwophasestate    | d
subdisableonerr     | f
subpasswordrequired | t
subrunasowner       | f
subfailover         | f
subconninfo         | host=10.223.125.57 port=5432 dbname=test user=replication password=secret
subslotname         | db_test_var_subscription
subsynccommit       | off
subpublications     | {test_var}
suborigin           | any

Managing the Publisher

test=# ALTER PUBLICATION my_publication ADD TABLE new_table;
test=# ALTER PUBLICATION my_publication DROP TABLE table;

Managing the Subscriber

Reasons:

  • Stop data synchronization
    • Resolving data conflicts (AI answer!)
      • manually fix duplicate keys, missing rows, or constraint violations (AI answer!)
    • Performing maintenance (AI answer!)
      • Pause heavy synchronization traffic during large index builds, schema migrations, or major upgrades on the subscriber.
    • Stopping infinite error loops (AI answer!)
    • Isolate network, CPU, or disk I/O bottlenecks on either the publisher or subscriber node. (AI answer!)
test=# ALTER SUBSCRIPTION db_test_var_subscription DISABLE;
test=# ALTER SUBSCRIPTION db_test_var_subscription ENABLE;

Adding a new table to Publication

On the Publisher add the new table to PUBLICATION:

test=# ALTER PUBLICATION test_var ADD table erp.test;

Create the table (and eventually also the schema) on the Subscriber:

test=# CREATE SCHEMA erp;
test=# CREATE TABLE erp.test (id SERIAL);

After adding the new table to Publication on has to run REFRESH PUBLICATION on the Subscriber:

test=# ALTER SUBSCRIPTION db_test_var_subscription REFRESH PUBLICATION;

In case you get the following error on the Publisher:

replication@test LOG:  logical decoding found consistent point at 0/1EA1708
replication@test DETAIL:  There are no running transactions.
replication@test STATEMENT:  CREATE_REPLICATION_SLOT "pg_16403_sync_16415_7672368634343456654" LOGICAL pgoutput (SNAPSHOT 'use')
replication@test ERROR:  permission denied for schema erp
replication@test STATEMENT:  COPY erp.test (id) TO STDOUT

you have to grant some additional privileges on the Publisher:

test=# GRANT USAGE ON SCHEMA erp TO replication;
test=# GRANT SELECT ON erp.test TO replication;

and then the error messages should magically disappear!


Monitoring on Subscriber

Check the error log on Publisher AND subscriber!

At least one per subscription:


test=# \x
Expanded display is on.

test=# SELECT * FROM pg_stat_subscription;
-[ RECORD 1 ]---------+------------------------------
subid                 | 16403
subname               | db_test_var_subscription
worker_type           | apply
pid                   | 4597
leader_pid            | 
relid                 | 
received_lsn          | 0/1C7CD90
last_msg_send_time    | 2026-08-10 13:34:22.642536+00
last_msg_receipt_time | 2026-08-10 13:34:22.64266+00
latest_end_lsn        | 0/1C7CD90
latest_end_time       | 2026-08-10 13:34:22.642536+00

See also Monitoring of Physical Replication: here and here



Limitations

DDL, Sequences, …

Fail-over

postgres=# SHOW sync_replication_slots;
postgres=# SELECT pg_sync_replication_slots();

Advanced topics

  • Replication filtering

Sources