PostgreSQL Logical Replication Sync
Use logical replication for selective sync in PostgreSQL — learn to replicate specific tables or rows, configure publications and subscriptions, and handle troubleshooting with this hands-on tutorial.
Focus: use logical replication for selective sync
You've built a perfect PostgreSQL schema, your indexes are tuned, and your queries fly — but now the business asks for a real-time analytics copy of just three tables from your production database, not the whole thing. You could dump and restore nightly, but that's stale by morning. Streaming the entire WAL to a standby would work, but it replicates everything, including that 2TB log table you don't need on the analytics box. This is where logical replication shines: it lets you pick exactly which tables — even which rows — get synced, with minimal impact on your primary. In this lesson, you'll learn how to use logical replication for selective sync, from the core concepts to a working hands-on setup, and how to avoid the gotchas that trip up even experienced DBAs.
The problem this lesson solves
Traditional PostgreSQL replication is physical: a standby replays the write-ahead log (WAL) byte-for-byte, producing an exact copy of the entire cluster. That's great for failover, but it's a blunt instrument for data distribution. You can't say, "replicate only the orders and customers tables to the reporting instance," or "only rows where region = 'EU'."
Shipping full dumps over the network is slow, and they're point-in-time snapshots — by the time they load, they're already outdated. This forces teams into a choice between stale analytics, massive bandwidth bills, or complex ETL pipelines that poll for changes.
Logical replication solves this by decoupling the what from the how. It uses a publish-subscribe model where a publication on the source defines which tables (and optionally which rows) to expose, and a subscription on the target pulls those changes in real time. You get selective, incremental sync without touching your application code or locking your tables.
Why now? Modern data pipelines demand near-real-time sync for dashboards, search indexes, or event-driven microservices. Logical replication is the native, low-maintenance answer built right into PostgreSQL — no extra tools required (for most use cases).
Core concept / mental model
Think of logical replication as a newsletter service for your database.
- The publisher (primary) runs a publication — like a mailing list — that says, "I'll send updates about these tables (and maybe only certain rows) to anyone who subscribes."
- The subscriber (standby or another database) runs a subscription — like signing up for that newsletter — and applies each change as it arrives.
- The wire protocol carries the changes as logical rows, not raw WAL bytes. That's why the subscriber can have a different schema (as long as columns match), a different PostgreSQL version, and even run on different hardware.
This is fundamentally different from physical streaming replication:
| Feature | Physical Replication | Logical Replication |
|---|---|---|
| Scope | Entire cluster | Selected tables (or rows) |
| Data format | WAL bytes | Logical row changes (INSERT/UPDATE/DELETE) |
| Schema changes | Not allowed on replica | Allowed, as long as compatible |
| Cross-version | Same major version | Can replicate to newer/older versions |
| Use case | High availability, failover | Selective sync, data distribution, migration |
Key definitions you'll encounter:
- Publication: A named object on the source that specifies which tables to publish. It can include all tables in a schema, specific tables, or filtered rows.
- Subscription: A named object on the target that connects to a publication and applies changes.
- Replication slot: On the publisher, it tracks the subscriber's progress in the WAL, ensuring changes aren't discarded until the subscriber confirms them.
How it works step by step
Setting up logical replication involves several moving parts. Here's the logical flow:
- Configure the publisher — set
wal_level = logicalinpostgresql.conf(or viaALTER SYSTEM), which adds the information needed for logical decoding. - Create a publication on the source, specifying which tables to publish. You can filter rows using a
WHEREclause. - Create a replication slot (optional — it's created automatically when you create a subscription, but you can also create one manually to manage it).
- On the target server, create the same tables (or compatible ones) that you want to receive.
- Create a subscription on the target, pointing to the connection string and publication name.
- The initial sync happens automatically: the subscription copies the current snapshot of the published tables, then starts streaming incremental changes.
- Monitor and manage — check
pg_stat_subscriptionandpg_subscription_relto see sync status.
Hands-on walkthrough
Let's walk through a complete example. Assume you have a primary database appdb with a table orders that you want to replicate selectively to a reporting database reports — only rows with status = 'paid'.
1. On the publisher (source)
First, set wal_level to logical. You can do this dynamically (since PostgreSQL 9.6) with:
ALTER SYSTEM SET wal_level = logical;
SELECT pg_reload_conf();
Pro tip: You don't need to restart the whole server —
pg_reload_conf()applies that change immediately. But if you're on an older version, you may need a restart.
Now create a publication for the orders table with a row filter:
-- On the publisher
CREATE TABLE orders (
id serial PRIMARY KEY,
customer_id int,
total numeric(10,2),
status text
);
INSERT INTO orders (customer_id, total, status) VALUES
(1, 99.99, 'paid'),
(2, 45.50, 'pending');
CREATE PUBLICATION orders_pub FOR TABLE orders WHERE (status = 'paid');
You can verify the publication:
SELECT * FROM pg_publication_tables;
-- pubname | schemaname | tablename
-- -------------+------------+-----------
-- orders_pub | public | orders
2. On the subscriber (target)
First, create the target table. It must have the same columns, but it can have different indexes or constraints. Include a primary key or a REPLICA IDENTITY if you want updates/deletes to propagate correctly.
-- On the subscriber
CREATE TABLE orders (
id integer PRIMARY KEY,
customer_id int,
total numeric(10,2),
status text
);
Now create a subscription that connects to the publication. Use a connection string that points to the publisher:
CREATE SUBSCRIPTION orders_sub
CONNECTION 'host=primary_host dbname=appdb user=replication_user password=secret'
PUBLICATION orders_pub;
PostgreSQL will automatically copy the current data (only rows where status='paid') and then start streaming changes.
3. Verify the sync
On the subscriber, check that data arrived:
SELECT * FROM orders;
-- id | customer_id | total | status
-- ----+-------------+-------+--------
-- 1 | 1 | 99.99 | paid
-- (1 row)
Only the paid order was replicated. Now, insert a new paid order on the publisher:
-- On publisher
INSERT INTO orders (customer_id, total, status) VALUES (3, 120.00, 'paid');
Wait a moment, then check again on the subscriber:
SELECT * FROM orders;
-- id | customer_id | total | status
-- ----+-------------+-------+--------
-- 1 | 1 | 99.99 | paid
-- 3 | 3 | 120.00| paid
-- (2 rows)
The new row appeared automatically — no manual intervention.
4. Wrapping up later
If you want to drop the subscription, you should disable it first to avoid orphaned replication slots:
ALTER SUBSCRIPTION orders_sub DISABLE;
ALTER SUBSCRIPTION orders_sub DROP;
Compare options / when to choose what
Logical replication is not the only way to sync data. Let's compare it with alternatives:
| Method | Best for | Pros | Cons |
|---|---|---|---|
| Logical replication | Selective sync, real-time analytics, data distribution | Fine-grained control, cross-version, low impact | Requires wal_level=logical, extra CPU on publisher, no DDL replication |
| Physical streaming replication | High availability, failover | Simple, full copy, synchronous option | Replicates everything, same version, no filtering |
| pg_dump / pg_restore | Point-in-time snapshots, small databases | Simple, portable | Stale, slow for big data, no incremental |
| Foreign Data Wrappers (FDW) | Querying remote data on demand | No materialization, flexible | Performance on large queries, no automatic sync |
| ETL tools (e.g., Airbyte, Debezium) | Complex transformations, many sources | Rich features | Extra infrastructure, maintenance |
When to choose logical replication:
- You need only a subset of tables or rows.
- You want near-real-time updates without polling.
- You are migrating between PostgreSQL versions.
- You can tolerate the target having a slightly different schema (e.g., extra indexes).
When NOT to choose it:
- You need DDL changes (like
ALTER TABLE) to replicate — they won't. - You need synchronous replication for failover (use physical for that).
- You have very high write throughput — logical decoding adds overhead.
Troubleshooting & edge cases
Even with a clean setup, things can go wrong. Here are common issues and fixes.
1. Subscription is stuck in "initializing"
If the subscription stays in that state, check:
SELECT * FROM pg_stat_subscription;
- Cause: The initial snapshot copy is happening, or there's a connection error.
- Fix: Check the publisher log for authentication issues. Ensure the replication user has
REPLICATIONprivilege:
CREATE USER replication_user WITH REPLICATION LOGIN PASSWORD 'secret';
GRANT CONNECT ON DATABASE appdb TO replication_user;
GRANT SELECT ON orders TO replication_user;
Also, make sure pg_hba.conf allows replication connections.
2. Updated rows disappear or don't update on the subscriber
This happens when the table has no primary key or replica identity. Logical replication uses the primary key to match rows for updates/deletes. Without one, updates become inserts or fail.
- Fix: Add a
PRIMARY KEYor at leastREPLICA IDENTITY USING INDEXon both tables. On the publisher, you can set:
ALTER TABLE orders REPLICA IDENTITY FULL;
This tells PostgreSQL to use all columns to identify rows — but it increases WAL size.
3. Subscription fails with "publication does not exist"
This usually means the subscription points to a publication name that doesn't exist on the publisher. Double-check names and case sensitivity.
4. Data type mismatches between publisher and subscriber
If the column types aren't compatible, apply errors will occur. Use the same types, or make sure PostgreSQL can cast between them.
5. Replication slot growing unboundedly
If the subscriber is offline for a long time, the replication slot on the publisher can grow, consuming disk. Monitor with:
SELECT * FROM pg_replication_slots;
If you don't need the slot anymore, drop it, or consider setting a max_slot_wal_keep_size.
What you learned & what's next
By now, you understand the core idea behind using logical replication for selective sync: a publication on the source defines what to share, and a subscription on the target pulls only those changes — table by table, row by row. You can explain the mental model (newsletter), set up a real publication with row filters, create a subscription, verify initial sync and live streaming, and troubleshoot common pitfalls like missing REPLICA IDENTITY or stuck subscriptions.
You've also completed a practical exercise — replicating only paid orders between two databases — which is a pattern you'll reuse for real-world scenarios like analytics replicas, microservice data sharing, or zero-downtime upgrades.
What's next? In the next lesson, you'll explore how to monitor and scale this replication, including handling schema changes, using multiple publications, and even upgrading to newer PostgreSQL versions with minimal downtime. Master selective sync, and you'll have a powerful tool for building responsive, distributed data systems.
Practice recap
Try a quick exercise: set up logical replication in two Docker containers or local PostgreSQL instances. Create a publication for a table with a filter, subscribe, insert a row that matches the filter and one that doesn't, and verify only the matching row appears. Then disable and drop the subscription, and observe the slot cleanup.
Common mistakes
- Forgetting to set
wal_level = logical— replication silently fails or doesn't start until you reload config. - Using a table without a primary key or
REPLICA IDENTITY— updates/deletes won't propagate correctly and may error. - Creating a subscription with a wrong connection string or publication name — get an obscure 'publication does not exist' error.
- Not granting the replication user
REPLICATIONprivilege or proper table SELECT grants, causing connection/apply failures. - Letting a replication slot grow unbounded when a subscriber is down — can fill your disk.
Variations
- Use a publication with
FOR ALL TABLESto replicate an entire schema, though that's less selective. - Use row filters like
WHERE status = 'paid'for selective row sync — as shown in the lesson. - Combine logical replication with
pg_dumpfor initial schema sync — since logical replication does not copy DDL changes.
Real-world use cases
- Replicating only the
orderstable to a separate analytics database for real-time reporting dashboards. - Sending just the
userstable from a primary OLTP database to a search service index. - Selectively syncing configuration tables to multiple regional read replicas for low-latency reads.
Key takeaways
- Logical replication lets you replicate only specific tables or rows — unlike physical replication which copies entire cluster.
- Use publications on the source and subscriptions on the target — no application code changes required.
- Row filters allow selective sync at the row level; you can even combine with column lists (not covered but possible).
- Always set
wal_level = logicaland use a replication user withREPLICATIONprivilege. - Tables need a primary key or
REPLICA IDENTITYfor correct update/delete propagation. - Monitor
pg_stat_subscriptionand replication slots to keep the sync healthy.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.