<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://illuminatedcomputing.com/">
  <id>https://illuminatedcomputing.com/</id>
  <title>Illuminated Computing</title>
  <updated>2024-10-13T00:00:00Z</updated>
  <link rel="alternate" href="https://illuminatedcomputing.com/" type="text/html"/>
  <link rel="self" href="https://illuminatedcomputing.com/tags/replication/atom.xml" type="application/atom+xml"/>
  <author>
    <name>Paul A. Jungwirth</name>
    <uri>https://illuminatedcommputing.com/</uri>
  </author>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-10-13:/posts/2024/10/postgres-replica-identity/</id>
    <title type="html">Postgres REPLICA IDENTITY</title>
    <published>2024-10-13T00:00:00Z</published>
    <updated>2024-10-13T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/10/postgres-replica-identity/" type="text/html"/>
    <content type="html">
&lt;p&gt;Both logical decoding and logical replication use a table’s &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY"&gt;&lt;code&gt;REPLICA IDENTITY&lt;/code&gt;&lt;/a&gt;. This is a way to say which row was changed by an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;. In other word it identifies the “old” row.&lt;/p&gt;

&lt;p&gt;Logical &lt;em&gt;decoding&lt;/em&gt; will use the replica identity to say which row was changed, if available, but if not then the information is simply omitted. No big deal.&lt;/p&gt;

&lt;p&gt;In logical &lt;em&gt;replication&lt;/em&gt;, the subscriber looks for the replica identity with each change, and it uses it to know which row to remove, either because it was replaced or because it was deleted. So you can always replicate inserts and truncates, but you can only replicate updates and deletes if you have an appropriate replica identity. In fact even on the publication side, Postgres will forbid changes without an appropriate replica identity, as we will see.&lt;/p&gt;

&lt;p&gt;Normally a table has &lt;code&gt;DEFAULT&lt;/code&gt; for its replica identity. This means it will use the table’s primary key (if present). You can’t set the replica identity when you create the table, but you can change it with &lt;code&gt;ALTER TABLE&lt;/code&gt;. Besides &lt;code&gt;DEFAULT&lt;/code&gt;, it can be &lt;code&gt;USING INDEX &amp;lt;index&amp;gt;&lt;/code&gt; or &lt;code&gt;FULL&lt;/code&gt; or &lt;code&gt;NOTHING&lt;/code&gt;. The replica identity is stored in &lt;code&gt;pg_class.relreplident&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had a lot of questions about how each of these works. Mostly I wanted to know when things failed: creating/altering the table/publication, making the change, or receiving it. All my tests were done on Postgres 17 using the &lt;code&gt;REL_17_0&lt;/code&gt; tag after doing &lt;code&gt;make world &amp;amp;&amp;amp; make install-world&lt;/code&gt;. I wanted to replicate from one database to another in the same cluster, but &lt;a href="https://www.postgresql.org/docs/current/sql-createsubscription.html#SQL-CREATESUBSCRIPTION-NOTES"&gt;that requires some (slightly) more complicated commands&lt;/a&gt;. To keep the SQL simple, I ran separate clusters, one with a database named &lt;code&gt;sender&lt;/code&gt;, the other with a database named &lt;code&gt;receiver&lt;/code&gt;. The sending cluster runs on 5432 to keep the receiver’s connection strings simpler.&lt;/p&gt;

&lt;p&gt;On the sender I set &lt;code&gt;wal_level&lt;/code&gt; like this (on a Mac):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sed -i '' -e '/wal_level/a\
wal_level=logical' ~/local/pgsql/data/postgresql.conf&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On Linux I think this would be just:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sed -i '/wal_level/awal_level=logical' ~/local/pgsql/data/postgresql.conf&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find the SQL for each test in &lt;a href="https://github.com/pjungwir/replica-identity-tests"&gt;this github repo&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=""&gt;&lt;code&gt;NOTHING&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Let’s start with &lt;code&gt;NOTHING&lt;/code&gt;. This means there is no information about the old row. If we’re looking for failures, this is nice and simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you create a publication for it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q2: What happens if you add it to an existing publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is allowed too:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p;
CREATE PUBLICATION
sender=# alter publication p add table t;
ALTER PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q3: What happens if you create a &lt;code&gt;FOR ALL TABLES&lt;/code&gt; publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And this is allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for all tables;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q4: What happens if you set a table to &lt;code&gt;NOTHING&lt;/code&gt; when it already belongs to a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# alter table t replica identity nothing;
ALTER TABLE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q5: What happens if you set a table to &lt;code&gt;NOTHING&lt;/code&gt; when you already have a &lt;code&gt;FOR ALL TABLES&lt;/code&gt; publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create publication p for all tables;
CREATE PUBLICATION
sender=# alter table t replica identity nothing;
ALTER TABLE&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q6: What happens if you change an insert-only publication to an update publication, and it has a &lt;code&gt;NOTHING&lt;/code&gt; table?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t with (publish = insert);
CREATE PUBLICATION
sender=# alter publication p set (publish = 'insert,update,delete,truncate');
ALTER PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q7: What happens if you change an insert-only &lt;code&gt;FOR ALL TABLES&lt;/code&gt; publication to an update publication, and the database has a &lt;code&gt;NOTHING&lt;/code&gt; table?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s allowed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for all tables with (publish = insert);
CREATE PUBLICATION
sender=# alter publication p set (publish = 'insert,update,delete,truncate');
ALTER PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q8: What happens if you update a &lt;code&gt;NOTHING&lt;/code&gt; table that is in a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s &lt;em&gt;not&lt;/em&gt; allowed!&lt;/p&gt;

&lt;p&gt;This fails on the publisher side, even if there is no subscription:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Q9: What happens if you delete from a &lt;code&gt;NOTHING&lt;/code&gt; table that is in a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This fails the same way:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity nothing;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot delete from table "t" because it does not have a replica identity and publishes deletes
HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So to summarize, Postgres doesn’t validate publications up front, but only when you try to send through them a &lt;code&gt;NOTHING&lt;/code&gt; update/delete.&lt;/p&gt;

&lt;h3 id="_2"&gt;&lt;code&gt;FULL&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FULL&lt;/code&gt; means all columns are combined to determine uniqueness. Of course that might &lt;em&gt;still&lt;/em&gt; not be unique. So what happens if you update one of them but not the other?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you have two identical records and you delete one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity full;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On the subscriber:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Back on the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# insert into t values ('a');
INSERT 0 1
sender=# select ctid, a from t;
 ctid  | a
-------+---
 (0,1) | a
 (0,2) | a
(2 rows)

sender=# delete from t where ctid = '(0,1)';
DELETE 1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the receiver sees:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;receiver=# select * from t;
 a 
---
 a
(1 row)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we didn’t lose both rows! I guess that’s because we are replicating a delete of one row. Which one we delete doesn’t matter, but we’ll only delete one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you have two identical records and you delete both?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity full;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On the subscriber:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Back on the publisher:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# insert into t values ('a');
INSERT 0 1
sender=# delete from t;
DELETE 2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the receiver sees:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;receiver=# select * from t;
 a 
---
(0 rows)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So both rows disappeared. That makes sense, because the receiver got two messages to delete a row like &lt;code&gt;('a')&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3: What happens if you have two identical records and you update one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I assume updating one of two identical rows will work the same, but let’s check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# alter table t replica identity full;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# insert into t values ('a');
INSERT 0 1
sender=# select ctid, a from t;
 ctid  | a
-------+---
 (0,1) | a
 (0,2) | a
(2 rows)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;sender=# update t set a = 'b' where ctid = '(0,1)';
UPDATE 1&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;receiver=# select * from t;
 a 
---
 a
 b
(2 rows)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yep!&lt;/p&gt;

&lt;p&gt;So with &lt;code&gt;FULL&lt;/code&gt; there are no errors on the publisher side nor on the subscriber side. The disadvantage is that everything is slower: logical decoding sends more data, and the subscriber must compare all the columns for equality.&lt;/p&gt;

&lt;p&gt;I wonder if the subscriber will still use an index to apply changes if there is one? I haven’t tested that yet, but if I do I will put an update here.&lt;/p&gt;

&lt;h3 id="_3"&gt;&lt;code&gt;USING INDEX &amp;lt;index&amp;gt;&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;This lets us choose a &lt;code&gt;UNIQUE&lt;/code&gt; index, as long as none of its keys are nullable. Assuming the subscriber has the same uniqueness, it can quickly locate which rows to change. Here is the happy path:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text not null unique);
CREATE TABLE
sender=# alter table t replica identity using index t_a_key;
ALTER TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In that case, &lt;code&gt;pg_class.relreplident&lt;/code&gt; is set to &lt;code&gt;i&lt;/code&gt;, and &lt;code&gt;pg_index.indisreplident&lt;/code&gt; is set to true for that index. You can see which index is used with &lt;code&gt;\d&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# \d t
                Table "public.t"
 Column | Type | Collation | Nullable | Default 
--------+------+-----------+----------+---------
 a      | text |           | not null | 
Indexes:
    "t_a_key" UNIQUE CONSTRAINT, btree (a) REPLICA IDENTITY&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But what can we do to mess it up?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q1: What happens if you use a not-unique index?&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create index idx_t_a on t (a);
CREATE INDEX
sender=# alter table t replica identity using index idx_t_a;
ERROR:  cannot use non-unique index "idx_t_a" as replica identity&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can’t even set the &lt;code&gt;REPLICA IDENTITY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2: What happens if you drop the index?&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text not null);
CREATE TABLE
sender=# create unique index idx_t_a on t (a);
CREATE INDEX
sender=# alter table t replica identity using index idx_t_a;
ALTER TABLE
sender=# drop index idx_t_a;
DROP INDEX
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Well you were able to drop it! The table’s &lt;code&gt;relreplident&lt;/code&gt; is still &lt;code&gt;i&lt;/code&gt;, but now there is no index with a true &lt;code&gt;pg_index.indisreplident&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s see what happens if we try to use it. On the subscriber:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Back on the sender:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Oops! We were able to insert, but the update failed. Even on the sender, we weren’t able to make the change.&lt;/p&gt;

&lt;p&gt;I’m dreaming of some other trickery with &lt;code&gt;ALTER INDEX&lt;/code&gt;, but nothing that seems problematic is supported, like changing a unique index to a non-unique one.&lt;/p&gt;

&lt;p&gt;So like &lt;code&gt;NOTHING&lt;/code&gt;, failures happen when we update the table. But in addition there is validation before setting &lt;code&gt;pg_index.indisreplident&lt;/code&gt;. If the index is not appropriate, we’ll fail there.&lt;/p&gt;

&lt;h3 id="_4"&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;DEFAULT&lt;/code&gt; setting means to use the table’s primary key, if it has one. So if the table has a primary key, you’re all good. But what if it doesn’t? Is it the same as &lt;code&gt;NOTHING&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Well we are allowed to set things up:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text);
CREATE TABLE
sender=# create publication p for table t;
CREATE PUBLICATION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And on the receiver:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;receiver=# create table t (a text);
CREATE TABLE
receiver=# create subscription s connection 'dbname=sender' publication p;
NOTICE:  created replication slot "s" on publisher
CREATE SUBSCRIPTION&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we make some changes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So again we can insert things, but not send updates. We fail before we even send over the change. In this test we didn’t even need to run anything on the receiver to cause the failure.&lt;/p&gt;

&lt;p&gt;Since the failure is so late, there is no difference between starting with a primary key and dropping it partway through. Likewise with other ideas we tried for &lt;code&gt;NOTHING&lt;/code&gt;, like adding the publication before the table or using &lt;code&gt;FOR ALL TABLES&lt;/code&gt;. Also changing an insert-only publication to an update publication is permitted, but then the next update command fails.&lt;/p&gt;

&lt;p&gt;Still let’s just try one simple case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2: What happens if you drop the primary key from a table in a publication?&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# create table t (a text primary key);
CREATE TABLE
sender=# create publication p for table t;
CREATE PUBLICATION
sender=# alter table t drop constraint t_pkey;
ALTER TABLE
sender=# insert into t values ('a');
INSERT 0 1
sender=# update t set a = 'b';
ERROR:  cannot update table "t" because it does not have a replica identity and publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No problem dropping the key, but then we get a failure making an update. Also this test shows that no subscription is required.&lt;/p&gt;

&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;So in all four cases, the validation happens when you update/delete a row and try to push the change through a &lt;code&gt;PUBLICATION&lt;/code&gt;. Your change gets rolled back, so there is no inconsistency. I’m happy about this, because it lowers the risk of tricking Postgres into publishing changes it shouldn’t. Also it makes things easier for replicating temporal tables. (Of course that was my ulterior motive!)&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:illuminatedcomputing.com,2024-10-13:/posts/2024/10/logical-replication/</id>
    <title type="html">Postgres Logical Replication</title>
    <published>2024-10-13T00:00:00Z</published>
    <updated>2024-10-13T00:00:00Z</updated>
    <link rel="alternate" href="https://illuminatedcomputing.com/posts/2024/10/logical-replication/" type="text/html"/>
    <content type="html">
&lt;p&gt;Logical &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding.html"&gt;decoding&lt;/a&gt; is different from logical &lt;a href="https://www.postgresql.org/docs/current/logical-replication.html"&gt;replication&lt;/a&gt;. Logical replication is built on logical decoding.&lt;/p&gt;

&lt;h2 id="logical_decoding"&gt;Logical Decoding&lt;/h2&gt;

&lt;p&gt;Logical decoding exports the changes happening in your database. They are streamed through a &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS"&gt;replication slot&lt;/a&gt;. The slot keeps track of how far you’ve read, so that it doesn’t skip or repeat messages. (Btw how is it not “at least once” delivery?) Basically you are getting a stream of WAL records. But whereas a &lt;em&gt;physical&lt;/em&gt; replication slot gives you the exact binary of the WAL as it gets written, a logical replication slot includes an &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-output-plugin.html"&gt;&lt;em&gt;output plugin&lt;/em&gt;&lt;/a&gt; that encodes the WAL in a (hopefully) more accessible way.&lt;/p&gt;

&lt;p&gt;You can write output plugins yourself by implementing various callbacks to handle different kinds of database activity. The only essential callbacks are &lt;code&gt;LogicalDecodeBeginCB&lt;/code&gt;, &lt;code&gt;LogicalDecodeChangeCB&lt;/code&gt;, and &lt;code&gt;LogicalDecodeCommitCB&lt;/code&gt;, but there are many optional ones. Some of those let you implement two-phase commit (see e.g. &lt;a href="https://www.amazon.com/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321"&gt;Kleppmann&lt;/a&gt; 352–359).&lt;/p&gt;

&lt;p&gt;You can also add an &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-writer.html"&gt;&lt;em&gt;output writer&lt;/em&gt;&lt;/a&gt;, but I’m not sure how it differs from a plugin. Somehow it only requires implementing three callbacks instead of many. I don’t see any way to “install” your writer or attach it to a slot. I’ll come back to this some other day.&lt;/p&gt;

&lt;p&gt;The most popular output plugin is &lt;a href="https://github.com/eulerto/wal2json"&gt;wal2json&lt;/a&gt;. When you read from the replication slot, you get each WAL record as a JSON object. Postgres also has a built-in &lt;code&gt;test_decoding&lt;/code&gt; output plugin (the default), which gives you the details as plain text.&lt;/p&gt;

&lt;p&gt;Postgres comes with a tool called &lt;code&gt;pg_recvlogical&lt;/code&gt; you can use to read from a replication slot. It can also create and drop slots. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pg_recvlogical --create-slot --if-not-exists --slot s --dbname paul
pg_recvlogical --start --slot s --dbname paul -f -&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you let that run then go into psql and issue some inserts/updates/deletes, you will see the messages getting sent through the slot.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;paul=# create table t (a text primary key);
CREATE TABLE
paul=# insert into t values ('a'), ('b');
INSERT 0 2
paul=# update t set a = 'aa' where a = 'a';
UPDATE 1
paul=# delete from t where a = 'b';
DELETE 1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our &lt;code&gt;pg_recvlogical&lt;/code&gt; command prints:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN 19715
COMMIT 19715
BEGIN 19716
table public.t: INSERT: a[text]:'a'
table public.t: INSERT: a[text]:'b'
COMMIT 19716
BEGIN 19717
table public.t: UPDATE: old-key: a[text]:'a' new-tuple: a[text]:'aa'
COMMIT 19717
BEGIN 19718
table public.t: DELETE: a[text]:'b'
COMMIT 19718&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you want JSON instead, you can create the slot with &lt;code&gt;-P wal2json&lt;/code&gt; (after installing it).&lt;/p&gt;

&lt;p&gt;The tricky part here is identifying which row changed for updates and deletes. Because &lt;code&gt;t&lt;/code&gt; had a primary key, Postgres used it, because it should uniquely identify a row on the other end. But you can do other things too, based on the table’s &lt;code&gt;REPLICA IDENTITY&lt;/code&gt;. You can set this to &lt;code&gt;DEFAULT&lt;/code&gt; (i.e. using the primary key if present), &lt;code&gt;NOTHING&lt;/code&gt;, &lt;code&gt;USING INDEX &amp;lt;index&amp;gt;&lt;/code&gt;, or &lt;code&gt;FULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NOTHING&lt;/code&gt; is the same as &lt;code&gt;DEFAULT&lt;/code&gt; with no primary key: there is no identifying information available.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;USING INDEX&lt;/code&gt; takes a unique index with no nullable parts, and uses that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FULL&lt;/code&gt; uses all the attributes of the row. It works for any table, but it’s less performant.&lt;/p&gt;

&lt;p&gt;For more details on &lt;code&gt;REPLICA IDENTITY&lt;/code&gt; you can read my article &lt;a href="/posts/2024/10/postgres-replica-identity/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s drop the table, create it without a primary key, and run the same commands again:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;paul=# create table t (a text);
CREATE TABLE
paul=# insert into t values ('a'), ('b');
INSERT 0 2
paul=# update t set a = 'aa' where a = 'a';
UPDATE 1
paul=# delete from t where a = 'b';
DELETE 1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the logical decoding output doesn’t have any old-row identifiers:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN 19720
COMMIT 19720
BEGIN 19721
table public.t: INSERT: a[text]:'a'
table public.t: INSERT: a[text]:'b'
COMMIT 19721
BEGIN 19722
table public.t: UPDATE: a[text]:'aa'
COMMIT 19722
BEGIN 19723
table public.t: DELETE: (no-tuple-data)
COMMIT 19723&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Missing a &lt;code&gt;REPLICA IDENTITY&lt;/code&gt; is no problem for logical decoding. For logical &lt;em&gt;replication&lt;/em&gt;, it will raise an error when you try to update or delete.&lt;/p&gt;

&lt;h2 id="clarification_of_streaming"&gt;Clarification of “Streaming”&lt;/h2&gt;

&lt;p&gt;In the Postgres docs, &lt;a href="https://www.postgresql.org/message-id/flat/CA%2BrenyULt3VBS1cRFKUfT2%3D5dr61xBOZdAZ-CqX3XLGXqY-aTQ%40mail.gmail.com"&gt;the word “streaming” can be misleading&lt;/a&gt;. Often it means &lt;em&gt;physical&lt;/em&gt; replication (originally the only kind of streaming replication we had), in contrast to logical—but not always.&lt;/p&gt;

&lt;p&gt;Sometimes “streaming replication” constrasts with &lt;a href="https://www.postgresql.org/docs/current/warm-standby.html"&gt;“log shipping”&lt;/a&gt;. It means the standby opens a connection to the primary and constantly pulls data via the &lt;a href="https://www.postgresql.org/docs/current/protocol-replication.html"&gt;streaming replication protocol&lt;/a&gt;. In log shipping, the primary has an &lt;code&gt;archive_command&lt;/code&gt; to copy WAL files where the standby can read them. (In fact you usually combine these methods.) That said, this page is at the same time equating streaming replication with physical. You &lt;em&gt;could&lt;/em&gt; set up a standby with logical replication, but that’s not what it’s describing.&lt;/p&gt;

&lt;p&gt;In another place, “streaming” means the streaming replication protocol in contrast to &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-sql.html"&gt;SQL commands&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Perhaps most often, “streaming replication” means “physical replication” in contrast to logical. For instance &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS"&gt;this note about logical replication slots&lt;/a&gt; should perhaps &lt;code&gt;s/streaming/physical/&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL also has streaming replication slots (see Section 26.2.5), but they are used somewhat differently there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Likewise the three opening paragraphs of the &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html"&gt;page for “Replication” configuration&lt;/a&gt; seem to constrast streaming replication to logical replication.&lt;/p&gt;

&lt;p&gt;So just watch out when you’re reading! More precise language would contast “physical” with “logical”, and be clear that both are “streaming.” I’m hopeful from that mailing list thread so far that we might start moving the docs in that direction.&lt;/p&gt;

&lt;h2 id="logical_replication"&gt;Logical Replication&lt;/h2&gt;

&lt;p&gt;Logical replication is built on top of logical decoding and replication slots. But instead of working at such a low level, it replicates changes from one Postgres table to another (usually in another cluster). The publisher side uses a built-in output plugin named &lt;code&gt;pgoutput&lt;/code&gt;. To set it up, you use &lt;a href="https://www.postgresql.org/docs/current/sql-createpublication.html"&gt;&lt;code&gt;CREATE PUBLICATION&lt;/code&gt;&lt;/a&gt; on the sender and &lt;a href="https://www.postgresql.org/docs/current/sql-createsubscription.html"&gt;&lt;code&gt;CREATE SUBSCRIPTION&lt;/code&gt;&lt;/a&gt; on the receiver. Like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# CREATE PUBLICATION p FOR TABLE t1, t2;
. . .
receiver=# CREATE SUBSCRIPTION s CONNECTION 'host=yonder dbname=thisnthat' PUBLICATION p;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But first you’ll need to have tables in the receiving database named &lt;code&gt;t1&lt;/code&gt; and &lt;code&gt;t2&lt;/code&gt;, with at least enough columns to match their sources.&lt;/p&gt;

&lt;p&gt;There are lots of options. For the publication, you can create it &lt;code&gt;FOR ALL TABLES&lt;/code&gt; or &lt;code&gt;FOR TABLES IN SCHEMA s1, s2&lt;/code&gt;. You can publish a subset of event types (&lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;truncate&lt;/code&gt;). You can publish a subset of each table’s columns. You can do special things with partitions. You can have a &lt;code&gt;WHERE&lt;/code&gt; clause to publish only certain rows (assuming the publication is for a single table).&lt;/p&gt;

&lt;p&gt;The subscription side has lots of options too. See the docs above for details.&lt;/p&gt;

&lt;p&gt;Once you have a publication and subscription created, you will see an entry in the sender’s &lt;code&gt;pg_stat_replication&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sender=# select * from pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 44344
usesysid         | 10
usename          | paul
application_name | s
client_addr      | NULL
client_hostname  | NULL
client_port      | -1
backend_start    | 2024-10-13 11:37:18.361684-05
backend_xmin     | NULL
state            | streaming
sent_lsn         | 0/11DE0AB8
write_lsn        | 0/11DE0AB8
flush_lsn        | 0/11DE0AB8
replay_lsn       | 0/11DE0AB8
write_lag        | NULL
flush_lag        | NULL
replay_lag       | NULL
sync_priority    | 0
sync_state       | async
reply_time       | 2024-10-13 11:37:48.419264-05&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Unlike physical replication, logical replication does not transfer DDL changes.&lt;/p&gt;

&lt;h3 id="synchronous_replication"&gt;Synchronous Replication&lt;/h3&gt;

&lt;p&gt;Both logical decoding and logical replication can be &lt;em&gt;synchronous&lt;/em&gt;. This means we can configure the primary to report successful commits only after hearing from the standby(s) that they were successful. (You can also do this with physical replication.) To enable this, first set &lt;code&gt;synchronous_standby_names&lt;/code&gt; to a comma-separated list of names you will wait for. Then set &lt;code&gt;synchronous_commit&lt;/code&gt; to &lt;code&gt;remote_apply&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;remote_write&lt;/code&gt;, or &lt;code&gt;local&lt;/code&gt;. (The default is &lt;code&gt;on&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;remote_apply&lt;/code&gt; is the most thorough: the standbys must flush the commit record to disk and apply it (so that other connections can see it). &lt;code&gt;on&lt;/code&gt; also requires flushing to disk (e.g. WAL), but it needn’t be applied. &lt;code&gt;remote_write&lt;/code&gt; requires standbys to perform the write to disk, but they need not have confirmation from the OS that it’s been flushed. And &lt;code&gt;local&lt;/code&gt; will not wait for standbys. (This is sort of pointless if you put things in &lt;code&gt;synchronous_standby_names&lt;/code&gt;, but maybe it lets you disable synchronous replication temporarily without erasing that other setting.)&lt;/p&gt;

&lt;p&gt;The names to put in &lt;code&gt;synchronous_standby_names&lt;/code&gt; should match the connection’s &lt;code&gt;application_name&lt;/code&gt;, one of the parameters available when opening any connection. You can set it in the connection string if you like. Otherwise it defaults to the subscription name. (For physical replication, standbys omitting this from their connection string default to their &lt;code&gt;cluster_name&lt;/code&gt; or failing that &lt;code&gt;walsender&lt;/code&gt;.) You can see the &lt;code&gt;application_name&lt;/code&gt; in &lt;code&gt;pg_stat_replication&lt;/code&gt; above matches the subscription name.&lt;/p&gt;

&lt;p&gt;If you have more than one standby listed as synchronous, the primary will wait on confirmation from &lt;em&gt;all&lt;/em&gt; of them (by default: you can do more nuanced things with &lt;code&gt;synchronous_standby_names&lt;/code&gt; if you like).&lt;/p&gt;

&lt;p&gt;If you are using synchronous logical replication, this is an &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-synchronous.html#LOGICALDECODING-SYNCHRONOUS-OVERVIEW"&gt;important warning&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A synchronous replica receiving changes via logical decoding will work in the scope of a single database. Since, in contrast to that, &lt;code&gt;synchronous_standby_names&lt;/code&gt; currently is server wide, this means this technique will not work properly if more than one database is actively used.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;According to &lt;a href="https://github.com/postgres/postgres/commit/3cb828dbe26087e7754f49f3cfe3ed036d5af439"&gt;commit 3cb828dbe2&lt;/a&gt; the problem is a potential deadlock from locking catalog tables. The commit and the docs have the specific lock sequences that are dangerous, and they don’t seem hard to avoid.&lt;/p&gt;

&lt;p&gt;My first impression on reading the note was a different scenario though. Consider: you have two databases, both replicated with logical replication, but each to a different standby. You make a change in one database, and now you’re waiting for both standbys to confirm. But one never got the change so will never answer.&lt;/p&gt;

&lt;p&gt;But in fact Postgres is smarter than that and only waits for the standbys that are relevant. I tested these scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One database in the sender cluster replicates to a database in the receiver cluster, and I update a table in a different database in the sender cluster. (I am “actively using” it.)&lt;/li&gt;

&lt;li&gt;Two databases in the sender cluster each replicate to the same database in the receiver cluster.&lt;/li&gt;

&lt;li&gt;Two databases in the sender cluster each replicate to databases in different clusters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="logical_replication_combined_with_physical_replication_failover"&gt;Logical Replication combined with Physical Replication Failover&lt;/h3&gt;

&lt;p&gt;If your primary cluster has both a physical replication standby and logical replication subscribers, and then you fail over to the standby, you need a way to point the logical subscribers at the newly-promoted cluster (i.e. the former standby). You want to do that without losing data.&lt;/p&gt;

&lt;p&gt;If (before the failure) you set &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-SYNC-REPLICATION-SLOTS"&gt;&lt;code&gt;sync_replication_slots&lt;/code&gt;&lt;/a&gt; to true on the physical standby, it will maintain the same slots the primary has, including keeping track of how far each has been read. That way when your logical subscribers connect to the new primary, they can resume where they left off.&lt;/p&gt;

&lt;p&gt;It is also a good idea to use &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-SYNCHRONIZED-STANDBY-SLOTS"&gt;&lt;code&gt;synchronized_standby_slots&lt;/code&gt;&lt;/a&gt;, to make sure the logical subscribers don’t get &lt;em&gt;ahead&lt;/em&gt; of the physical standby.&lt;/p&gt;
</content>
  </entry>
</feed>

