Логическая репликация

Логическая репликация — это метод репликации объектов данных и изменений в них, основанный на их репликационных идентификаторах (обычно это первичный ключ). Мы используем термин «логическая» как противопоставление физической репликации, использующей точные адреса блоков и побайтовое копирование. QHB поддерживает одновременно оба механизма; см. главу Высокая доступность, балансировка нагрузки и репликация. Логическая репликация позволяет более детально управлять как репликацией данных, так и вопросами безопасности.

В логической репликации используется модель публикаций и подписок с одним или несколькими подписчиками на одну или несколько публикаций на узле-публикаторе. Подписчики извлекают данные из публикаций, на которые они подписаны, и затем могут повторно публиковать данные для обеспечения каскадной репликации или более сложных конфигураций.

Логическая репликация таблицы обычно начинается с создания снимка данных в публикующей базе данных и копирования его подписчику. После этого изменения на стороне публикатора передаются подписчику тогда, когда они происходят, в реальном времени. Подписчик применяет эти данные в том же порядке, что и публикатор, поэтому для публикаций в рамках одной подписки гарантируется транзакционная согласованность. Этот метод репликации данных иногда называется транзакционной репликацией.

Типичные сценарии использования логической репликации:

  • Передача подписчикам инкрементальных изменений в одной базе данных или подмножестве базы данных, когда они происходят.

  • Срабатывание триггеров для отдельных изменений, когда они поступают к подписчику.

  • Объединение нескольких баз данных в одну (например, в целях анализа).

  • Репликация между разными основными версиями QHB.

  • Репликация между экземплярами QHB на разных платформах.

  • Предоставление доступа к реплицированным данным разным группам пользователей.

  • Разделение подмножества базы данных между несколькими базами данных.

База-подписчик ведет себя так же, как и любой другой экземпляр QHB, и может использоваться как публикатор для других баз данных, определив собственные публикации. Когда подписчик воспринимается приложением как исключительно читающий, никаких конфликтов с одной подпиской не будет. С другой стороны, конфликты могут возникнуть, если приложение или другие подписчики производят запись в тот же набор таблиц.



Публикация

Публикацию можно определить на любом основном сервере физической репликации. Узел, где определяется публикация, называется публикатором. Публикация — это набор изменений, сгенерированных в таблице или в группе таблиц, который также можно описать как просто набор изменений или набор репликации. Каждая публикация существует только в одной базе данных.

Публикации отличаются от схем и не влияют на доступ к таблице. При необходимости каждую таблицу можно добавить в несколько публикаций. В настоящее время публикации могут содержать только таблицы. Объекты в них нужно добавлять явно, за исключением случаев, когда публикация создана для всех таблиц (с указанием ALL TABLES).

При желании публикации могут ограничить выдаваемые изменения любым сочетанием операций INSERT, UPDATE, DELETE и TRUNCATE подобно тому, как триггеры срабатывают для конкретных типов событий. По умолчанию реплицируются все типы операций. Эти спецификации публикаций применяются только к операциям DML; они не влияют на копирование при начальной синхронизации данных. (Фильтры строк не действуют на TRUNCATE. См. раздел Фильтры строк).

Чтобы иметь возможность реплицировать операции UPDATE и DELETE, в публикуемой таблице должен быть сконфигурирован «репликационный идентификатор», чтобы на стороне подписчика можно было определить соответствующие строки для изменения или удаления. По умолчанию это первичный ключ, если таковой имеется. Кроме того, в качестве репликационного идентификатора можно установить другой уникальный индекс (с некоторыми дополнительными условиями). Если у таблицы нет подходящего ключа, в качестве репликационного идентификатора можно установить «full», что означает, что ключом станет вся строка. Однако это крайне неэффективно и должно использоваться только как запасной вариант, если другого решения нет. Если на стороне публикатора установлен репликационный идентификатор, отличный от «full», то идентификатор, состоящий из того же или меньшего количества столбцов, должен быть также определен на стороне подписчика. Более подробную информацию о том, как установить репликационный идентификатор, см. в описании параметра REPLICA IDENTITY. Если в публикацию, которая реплицирует операции UPDATE или DELETE, добавляется таблица без репликационного идентификатора, то последующие операции UPDATE и DELETE вызовут ошибку на стороне публикатора. Операции INSERT могут выполняться вне зависимости от репликационного идентификатора.

У каждой публикации может быть множество подписчиков.

Публикация создается с помощью команды CREATE PUBLICATION и впоследствии может быть изменена или удалена с помощью соответствующих команд.

С помощью команды ALTER PUBLICATION в публикацию можно динамически добавлять или удалять отдельные таблицы. Операции ADD TABLE и DROP TABLE являются транзакционными, поэтому репликация таблицы начнется и закончится с корректным снимком только после фиксации транзакции.



Подписка

Подписка — это принимающая сторона логической репликации. Узел, где определяется подписка, называется подписчиком. В подписке определяется подключение к другой базе данных и набор публикаций (из одной или нескольких), на которые подписчик хочет подписаться.

База-подписчик ведет себя так же, как и любой другой экземпляр QHB, и может использоваться как публикатор для других баз данных, определив собственные публикации.

Узел-подписчик при желании может иметь несколько подписок. В одной паре публикатор- подписчик можно определить несколько подписок, но в этом случае следует позаботиться о том, чтобы публикуемые объекты в разных подписках не перекрывались.

Каждая подписка будет получать изменения через один слот репликации (см. подраздел Слоты репликации). Для начальной синхронизации данных, уже существующих в таблицах, могут потребоваться дополнительные слоты репликации, но они будут удалены в конце синхронизации.

Подписка логической репликации может быть резервным сервером для синхронной репликации (см. подраздел Синхронная репликация). При этом по умолчанию именем резервного сервера будет имя подписки. Другое имя можно задать в параметре application_name в строке подключения этой подписки.

Подписки сбрасываются на диск утилитой qhb_dump, если текущий пользователь является суперпользователем. В противном случае выдается предупреждение и подписки пропускаются, потому что обычные пользователи не могут читать всю информацию о подписках из каталога pg_subscription.

Подписка добавляется с помощью команды CREATE SUBSCRIPTION и в любой момент может быть остановлена/возобновлена с помощью команды ALTER SUBSCRIPTION и удалена с помощью команды DROP SUBSCRIPTION.

Когда подписка удаляется и пересоздается, информация о синхронизации теряется. Это означает, что после этого данные нужно синхронизировать заново.

Определения схем не реплицируются, а публикуемые таблицы должны существовать на подписчике. Целью репликации могут быть только обычные таблицы. Например, нельзя реплицировать в представление.

Таблицы сопоставляются между публикатором и подписчиком по полностью уточненным именам. Репликация в таблицу с другим именем на стороне подписчика не поддерживается.

Столбцы таблиц тоже сопоставляются по именам. Порядок столбцов в таблице подписчика не обязан совпадать с порядком столбцов у публикатора. Типам данных столбцов тоже не обязательно совпадать, если текстовое представление данных можно преобразовать в целевой тип. К примеру, можно реплицировать из столбца типа integer в столбец типа bigint. Кроме того, целевая таблица может содержать дополнительные столбцы, отсутствующие в таблице публикатора. Все эти столбцы будут заполнены значениями по умолчанию, заданными в определении целевой таблицы.


Управление слотами репликации

Как уже упоминалось ранее, каждая (активная) подписка получает изменения из слота репликации на удаленной (публикующей) стороне.

Дополнительные слоты синхронизации таблиц обычно являются временными, создаваемыми внутренне для проведения начальной синхронизации таблиц и автоматически удаляются, когда становятся не нужны. У этих слотов синхронизации таблиц генерируются имена вида «pg_%u_sync_%u_%llu» (параметры: oid подписки, relid таблицы, системный идентификатор sysid).

Обычно удаленный слот репликации создается автоматически, когда подписка создается с помощью команды CREATE SUBSCRIPTION, и автоматически же удаляется при удалении подписки командой DROP SUBSCRIPTION. Однако в некоторых ситуациях может быть полезно или даже необходимо манипулировать подпиской и нижележащим слотом репликации по отдельности. Ниже приведены некоторые возможные сценарии:

  • При создании подписки слот репликации уже существует. В этом случае подписку можно создать с параметром create_slot = false, чтобы связать ее с существующим слотом.

  • При создании подписки удаленный хост недоступен или находится в неопределенном состоянии. В этом случае подписку можно создать с параметром connect = false. Тогда с удаленным хостом вообще не будет устанавливаться соединение. Этот вариант использует qhb_dump. Тогда для активации подписки удаленный слот репликации нужно будет создавать вручную.

  • При удалении подписки нужно сохранить слот репликации. Это может быть полезно, когда база-подписчик переносится на другой хост и будет активироваться оттуда. В этом случае перед удалением подписки следует отвязать от нее слот с помощью команды ALTER SUBSCRIPTION.

  • При удалении подписки удаленный хост недоступен. В этом случае перед удалением подписки следует отвязать от нее слот с помощью команды ALTER SUBSCRIPTION. Если удаленный экземпляр базы данных уже не существует, в дальнейших действиях нет необходимости. Однако если удаленный экземпляр базы данных просто недоступен, слот репликации (и все оставшиеся слоты синхронизации таблиц) нужно будет удалить вручную; в противном случае они продолжат сохранять WAL и в итоге могут вызвать переполнение диска. Такие случаи следует разбирать особенно тщательно.


Примеры подписок

Создайте нескольких тестовых таблиц на публикаторе.

test_pub=# CREATE TABLE t1(a int, b text, PRIMARY KEY(a));
CREATE TABLE
test_pub=# CREATE TABLE t2(c int, d text, PRIMARY KEY(c));
CREATE TABLE
test_pub=# CREATE TABLE t3(e int, f text, PRIMARY KEY(e));
CREATE TABLE

Создайте такие же таблицы на подписчике.

test_sub=# CREATE TABLE t1(a int, b text, PRIMARY KEY(a));
CREATE TABLE
test_sub=# CREATE TABLE t2(c int, d text, PRIMARY KEY(c));
CREATE TABLE
test_sub=# CREATE TABLE t3(e int, f text, PRIMARY KEY(e));
CREATE TABLE

Добавьте данные в таблице на стороне публикатора.

test_pub=# INSERT INTO t1 VALUES (1, 'one'), (2, 'two'), (3, 'three');
INSERT 0 3
test_pub=# INSERT INTO t2 VALUES (1, 'A'), (2, 'B'), (3, 'C');
INSERT 0 3
test_pub=# INSERT INTO t3 VALUES (1, 'i'), (2, 'ii'), (3, 'iii');
INSERT 0 3

Создайте публикации для таблиц. Для публикаций pub2 и pub3a запрещены некоторые операции publish. У публикации pub3b имеется фильтр строк (см. раздел Фильтры строк).

test_pub=# CREATE PUBLICATION pub1 FOR TABLE t1;
CREATE PUBLICATION
test_pub=# CREATE PUBLICATION pub2 FOR TABLE t2 WITH (publish = 'truncate');
CREATE PUBLICATION
test_pub=# CREATE PUBLICATION pub3a FOR TABLE t3 WITH (publish = 'truncate');
CREATE PUBLICATION
test_pub=# CREATE PUBLICATION pub3b FOR TABLE t3 WHERE (e > 5);
CREATE PUBLICATION

Создайте подписки на публикации. Подписка sub3 оформляется на две публикации, pub3a и pub3b. Все подписки по умолчанию будут копировать начальные данные.

test_sub=# CREATE SUBSCRIPTION sub1
test_sub-# CONNECTION 'host=localhost dbname=test_pub application_name=sub1'
test_sub-# PUBLICATION pub1;
CREATE SUBSCRIPTION
test_sub=# CREATE SUBSCRIPTION sub2
test_sub-# CONNECTION 'host=localhost dbname=test_pub application_name=sub2'
test_sub-# PUBLICATION pub2;
CREATE SUBSCRIPTION
test_sub=# CREATE SUBSCRIPTION sub3
test_sub-# CONNECTION 'host=localhost dbname=test_pub application_name=sub3'
test_sub-# PUBLICATION pub3a, pub3b;
CREATE SUBSCRIPTION

Обратите внимание, что начальные данные таблицы копируются независимо от варианта операции publish в подписке.

test_sub=# SELECT * FROM t1;
 a |   b
---+-------
 1 | one
 2 | two
 3 | three
(3 rows)

test_sub=# SELECT * FROM t2;
 c | d
---+---
 1 | A
 2 | B
 3 | C
(3 rows)

Более того, поскольку при копировании начальных данных операция publish игнорируется, а у публикации pub3a нет фильтра строк, скопированная таблица t3 содержит все строки, даже если они не соответствуют фильтру строк публикации pub3b.

test_sub=# SELECT * FROM t3;
 e |  f
---+-----
 1 | i
 2 | ii
 3 | iii
(3 rows)

Добавьте еще данных в таблицу на стороне публикатора.

test_pub=# INSERT INTO t1 VALUES (4, 'four'), (5, 'five'), (6, 'six');
INSERT 0 3
test_pub=# INSERT INTO t2 VALUES (4, 'D'), (5, 'E'), (6, 'F');
INSERT 0 3
test_pub=# INSERT INTO t3 VALUES (4, 'iv'), (5, 'v'), (6, 'vi');
INSERT 0 3

Теперь данные на стороне публикатора выглядят так:

test_pub=# SELECT * FROM t1;
 a |   b
---+-------
 1 | one
 2 | two
 3 | three
 4 | four
 5 | five
 6 | six
(6 rows)

test_pub=# SELECT * FROM t2;
 c | d
---+---
 1 | A
 2 | B
 3 | C
 4 | D
 5 | E
 6 | F
(6 rows)

test_pub=# SELECT * FROM t3;
 e |  f
---+-----
 1 | i
 2 | ii
 3 | iii
 4 | iv
 5 | v
 6 | vi
(6 rows)

Обратите внимание, что во время обычной репликации применяются соответствующие операции publish. Это означает, что публикации pub2 и pub3a не будут реплицировать операции INSERT. Кроме того, публикация pub3b будет реплицировать только данные, соответствующие фильтру строк pub3b. Теперь данные на стороне подписчика выглядят так:

test_sub=# SELECT * FROM t1;
 a |   b
---+-------
 1 | one
 2 | two
 3 | three
 4 | four
 5 | five
 6 | six
(6 rows)

test_sub=# SELECT * FROM t2;
 c | d
---+---
 1 | A
 2 | B
 3 | C
(3 rows)

test_sub=# SELECT * FROM t3;
 e |  f
---+-----
 1 | i
 2 | ii
 3 | iii
 6 | vi
(4 rows)


Фильтры строк

По умолчанию все данные из всех опубликованных таблиц будут реплицироваться надлежащим подписчикам. Объем реплицируемых данных можно уменьшить с помощью фильтра строк. Пользователю может потребоваться воспользоваться фильтрами строк по соображениям функциональности, безопасности или производительности. Если для публикуемой таблицы задается фильтр строк, строка реплицируется, только если ее данные удовлетворяют выражению этого фильтра. Это позволяет обеспечить частичную репликацию набора таблиц. Фильтр строк определяется отдельно для каждой таблицы. Добавьте выражение WHERE после имени таблицы для каждой публикуемой таблицы, данные которой нужно отфильтровать. Предложение WHERE следует заключить в круглые скобки. Подробную информацию см. на справочной странице команды CREATE PUBLICATION.


Правила фильтров строк

Фильтры строк применяются перед публикацией изменений. Если результатом вычисления в фильтре строк является false или NULL, такая строка не реплицируется. Выражение предложения WHERE вычисляется от имени той же роли, что используется для подключения репликации (т. е. роли, указанной в предложении CONNECTION команды CREATE SUBSCRIPTION). На команду TRUNCATE фильтры строк не действуют.


Ограничения выражений

Предложение WHERE допускает только простые выражения. Оно не может содержать пользовательские функции, операторы, типы и правила сортировки, ссылки на системные столбцы или непостоянные встроенные функции.

Если в публикации публикуются операции UPDATE или DELETE, предложение WHERE фильтра строк должно содержать только те столбцы, которые входят в идентификатор реплики (см. REPLICA IDENTITY). Если в публикации публикуются только операции INSERT, предложение WHERE фильтра строк может использовать любой столбец.


Преобразования UPDATE

При каждом выполнении операции UPDATE выражение фильтра строк вычисляется как для старой, так и для новой строки (т. е. используя данные до и после обновления). Если оба результата равны true, изменение UPDATE реплицируется. Если оба результата равны false, изменение не реплицируется. Если только одна из строк (старая или новая) соответствует выражению фильтра, UPDATE преобразуется в INSERT или DELETE, чтобы избежать несогласованности данных. Строка на подписчике должна отражать то, что определяется выражением фильтра строк на публикаторе.

Если старая строка удовлетворяет выражению фильтра строк (она была передана подписчику), а новая строка — нет, то в интересах согласованности данных следует удалить старую строку с подписчика. Поэтому UPDATE преобразуется в DELETE.

Если старая строка не удовлетворяет выражению фильтра строк (она не была передана подписчику), а новая строка удовлетворяет, то в интересах согласованности данных следует добавить новую строку на подписчик. Поэтому UPDATE преобразуется в INSERT.

В Таблице 1 обобщенно представлены применяемые преобразования.

Таблица 1. Краткая сводка по преобразованию UPDATE

Старая строкаНовая строкаПреобразование
не соответствуетне соответствуетне реплицировать
не соответствуетсоответствуетINSERT
соответствуетне соответствуетDELETE
соответствуетсоответствуетUPDATE

Партиционированные таблицы

Если публикация содержит партиционированную таблицу, параметр публикации publish_via_partition_root определяет, какой фильтр строк использовать. Если publish_via_partition_root имеет значение true, используется фильтр строк корневой партиционированной таблицы. В противном же случае, если publish_via_partition_root имеет значение false (по умолчанию), для каждой партиции используется ее собственный фильтр строк.


Начальная синхронизация данных

Если подписка требует копирования уже существующих данных таблицы, а публикация содержит предложения WHERE, на подписчик копируются только те данные, которые удовлетворяют выражениям фильтров строк.

Если у подписки имеется несколько публикаций, в которых одна таблица была опубликована с разными предложениями WHERE, будут скопированы строки, удовлетворяющие любому из выражений. Подробную информацию см. в подразделе Объединение нескольких фильтров строк.

ПРЕДУПРЕЖДЕНИЕ
Поскольку при начальной синхронизации данных, когда копируются существующие данные таблиц, не учитывается параметр publish, могут быть скопированы строки, которые не реплицировались бы при выполнении команд DML. Примеры см. в подразделах Начальный снимок и Примеры списков столбцов.

Примечание
Если версия подписчика ниже 1.5.2, то при копировании существующих данных фильтры строк не используются, даже если они определены в публикации. Это связано с тем, что старые версии могут копировать данные таблицы только полностью.


Объединение нескольких фильтров строк

Если у подписки имеется несколько публикаций, в которых одна и та же таблица была опубликована с разными фильтрами строк (для одной и той же операции publish), эти выражения объединяются вместе логическим сложением, так что реплицироваться будут строки, удовлетворяющие любому из выражений. Это означает, что все остальные фильтры строк для той же таблицы становятся избыточными, если:

  • У одной из публикаций нет фильтра строк.

  • Одна из публикаций была создана с использованием FOR ALL TABLES. Это предложение не допускает применения фильтров строк.

  • Одна из публикаций была создана с использованием FOR TABLES IN SCHEMA, и вышеупомянутая таблица принадлежит указанной схеме. Это предложение не допускает применения фильтров строк.


Примеры фильтров строк

Создайте несколько таблиц для использования в следующих примерах.

test_pub=# CREATE TABLE t1(a int, b int, c text, PRIMARY KEY(a,c));
CREATE TABLE
test_pub=# CREATE TABLE t2(d int, e int, f int, PRIMARY KEY(d));
CREATE TABLE
test_pub=# CREATE TABLE t3(g int, h int, i int, PRIMARY KEY(g));
CREATE TABLE

Создайте несколько публикаций. В публикации p1 имеется одна таблица (t1), и у этой таблицы есть фильтр строк. В публикации p2 имеются две таблицы. У таблицы t1 нет фильтра строк, а у таблицы t2 — есть. В публикации p3 имеется две таблицы, и у обеих есть фильтр строк.

test_pub=# CREATE PUBLICATION p1 FOR TABLE t1 WHERE (a > 5 AND c = 'NSW');
CREATE PUBLICATION
test_pub=# CREATE PUBLICATION p2 FOR TABLE t1, t2 WHERE (e = 99);
CREATE PUBLICATION
test_pub=# CREATE PUBLICATION p3 FOR TABLE t2 WHERE (d = 10), t3 WHERE (g = 10);
CREATE PUBLICATION

Чтобы вывести выражения фильтров строк (если определены) для каждой публикации, можно воспользоваться утилитой psql.

test_pub=# \dRp+
                               Publication p1
  Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
--------+------------+---------+---------+---------+-----------+----------
 qhb    | f          | t       | t       | t       | t         | f
Tables:
    "public.t1" WHERE ((a > 5) AND (c = 'NSW'::text))

                               Publication p2
  Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
--------+------------+---------+---------+---------+-----------+----------
 qhb    | f          | t       | t       | t       | t         | f
Tables:
    "public.t1"
    "public.t2" WHERE (e = 99)

                               Publication p3
  Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
--------+------------+---------+---------+---------+-----------+----------
 qhb    | f          | t       | t       | t       | t         | f
Tables:
    "public.t2" WHERE (d = 10)
    "public.t3" WHERE (g = 10)

Также с помощью psql можно вывести выражения фильтров строк (если определены) для каждой таблицы. Обратите внимание, что таблица t1 является членом двух публикаций, но имеет фильтр строк только в p1, а таблица t2 является членом двух публикаций и имеет разные фильтры строк для каждой из них.

test_pub=# \d t1
                 Table "public.t1"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           | not null |
 b      | integer |           |          |
 c      | text    |           | not null |
Indexes:
    "t1_pkey" PRIMARY KEY, btree (a, c)
Publications:
    "p1" WHERE ((a > 5) AND (c = 'NSW'::text))
    "p2"

test_pub=# \d t2
                 Table "public.t2"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 d      | integer |           | not null |
 e      | integer |           |          |
 f      | integer |           |          |
Indexes:
    "t2_pkey" PRIMARY KEY, btree (d)
Publications:
    "p2" WHERE (e = 99)
    "p3" WHERE (d = 10)

test_pub=# \d t3
                 Table "public.t3"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 g      | integer |           | not null |
 h      | integer |           |          |
 i      | integer |           |          |
Indexes:
    "t3_pkey" PRIMARY KEY, btree (g)
Publications:
    "p3" WHERE (g = 10)

Создайте на узле-подписчике таблицу t1 с тем же определением, что и на публикаторе, а также создайте подписку s1, которая подписана на публикацию p1.

test_sub=# CREATE TABLE t1(a int, b int, c text, PRIMARY KEY(a,c));
CREATE TABLE
test_sub=# CREATE SUBSCRIPTION s1
test_sub-# CONNECTION 'host=localhost dbname=test_pub application_name=s1'
test_sub-# PUBLICATION p1;
CREATE SUBSCRIPTION

Вставьте несколько строк. Реплицируются только строки, удовлетворяющие предложению t1 WHERE публикации p1.

test_pub=# INSERT INTO t1 VALUES (2, 102, 'NSW');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (3, 103, 'QLD');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (4, 104, 'VIC');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (5, 105, 'ACT');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (6, 106, 'NSW');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (7, 107, 'NT');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (8, 108, 'QLD');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES (9, 109, 'NSW');
INSERT 0 1

test_pub=# SELECT * FROM t1;
 a |  b  |  c
---+-----+-----
 2 | 102 | NSW
 3 | 103 | QLD
 4 | 104 | VIC
 5 | 105 | ACT
 6 | 106 | NSW
 7 | 107 | NT
 8 | 108 | QLD
 9 | 109 | NSW
(8 rows)
test_sub=# SELECT * FROM t1;
 a |  b  |  c
---+-----+-----
 6 | 106 | NSW
 9 | 109 | NSW
(2 rows)

Измените некоторые данные, в которых старое и новое значения соответствуют предложению t1 WHERE публикации p1. Операция UPDATE реплицирует эти изменения как обычно.

test_pub=# UPDATE t1 SET b = 999 WHERE a = 6;
UPDATE 1

test_pub=# SELECT * FROM t1;
 a |  b  |  c
---+-----+-----
 2 | 102 | NSW
 3 | 103 | QLD
 4 | 104 | VIC
 5 | 105 | ACT
 7 | 107 | NT
 8 | 108 | QLD
 9 | 109 | NSW
 6 | 999 | NSW
(8 rows)
test_sub=# SELECT * FROM t1;
 a |  b  |  c
---+-----+-----
 9 | 109 | NSW
 6 | 999 | NSW
(2 rows)

Измените некоторые данные, в которых старые значения строк не удовлетворяют предложению t1 WHERE публикации p1, а новые значения строк — удовлетворяют. Операция UPDATE преобразуется в INSERT, и уже эти изменения реплицируются. Обратите внимание на новую строку на подписчике.

test_pub=# UPDATE t1 SET a = 555 WHERE a = 2;
UPDATE 1

test_pub=# SELECT * FROM t1;
  a  |  b  |  c
-----+-----+-----
   3 | 103 | QLD
   4 | 104 | VIC
   5 | 105 | ACT
   7 | 107 | NT
   8 | 108 | QLD
   9 | 109 | NSW
   6 | 999 | NSW
 555 | 102 | NSW
(8 rows)
test_sub=# SELECT * FROM t1;
  a  |  b  |  c
-----+-----+-----
   9 | 109 | NSW
   6 | 999 | NSW
 555 | 102 | NSW
(3 rows)

Измените некоторые данные, в которых старые значения строк удовлетворяют предложению t1 WHERE публикации p1, а новые значения строк — не удовлетворяют. Операция UPDATE преобразуется в DELETE, и уже эти изменения реплицируются. Обратите внимание, что строка была удалена с подписчика.

test_pub=# UPDATE t1 SET c = 'VIC' WHERE a = 9;
UPDATE 1

test_pub=# SELECT * FROM t1;
  a  |  b  |  c
-----+-----+-----
   3 | 103 | QLD
   4 | 104 | VIC
   5 | 105 | ACT
   7 | 107 | NT
   8 | 108 | QLD
   6 | 999 | NSW
 555 | 102 | NSW
   9 | 109 | VIC
(8 rows)
test_sub=# SELECT * FROM t1;
  a  |  b  |  c
-----+-----+-----
   6 | 999 | NSW
 555 | 102 | NSW
(2 rows)

Следующие примеры показывают, как параметр публикации publish_via_partition_root определяет, какой фильтр строк будет использоваться в случае партиционированных таблиц: родительской таблицы или дочерней.

Создайте партиционированную таблицу на публикаторе.

test_pub=# CREATE TABLE parent(a int PRIMARY KEY) PARTITION BY RANGE(a);
CREATE TABLE
test_pub=# CREATE TABLE child PARTITION OF parent DEFAULT;
CREATE TABLE

Создайте такие же таблицы на подписчике.

test_sub=# CREATE TABLE parent(a int PRIMARY KEY) PARTITION BY RANGE(a);
CREATE TABLE
test_sub=# CREATE TABLE child PARTITION OF parent DEFAULT;
CREATE TABLE

Создайте публикацию p4, а затем подпишитесь на нее. В параметре публикации publish_via_partition_root установлено значение true. При этом фильтры строк определены как для партиционированной таблице (parent), так и для партиции (child).

test_pub=# CREATE PUBLICATION p4 FOR TABLE parent WHERE (a < 5), child WHERE (a >= 5)
test_pub-# WITH (publish_via_partition_root=true);
CREATE PUBLICATION
test_sub=# CREATE SUBSCRIPTION s4
test_sub-# CONNECTION 'host=localhost dbname=test_pub application_name=s4'
test_sub-# PUBLICATION p4;
CREATE SUBSCRIPTION

Вставьте несколько значений непосредственно в таблицы parent и child. Они реплицируются с использованием фильтра строк parent (потому что параметр publish_via_partition_root имеет значение true).

test_pub=# INSERT INTO parent VALUES (2), (4), (6);
INSERT 0 3
test_pub=# INSERT INTO child VALUES (3), (5), (7);
INSERT 0 3

test_pub=# SELECT * FROM parent ORDER BY a;
 a
---
 2
 3
 4
 5
 6
 7
(6 rows)
test_sub=# SELECT * FROM parent ORDER BY a;
 a
---
 2
 3
 4
(3 rows)

Повторите тот же тест, но с другим значением для publish_via_partition_root. Теперь в параметре публикации publish_via_partition_root установлено значение false. Фильтр строк определяется для партиции (child).

test_pub=# DROP PUBLICATION p4;
DROP PUBLICATION
test_pub=# CREATE PUBLICATION p4 FOR TABLE parent, child WHERE (a >= 5)
test_pub-# WITH (publish_via_partition_root=false);
CREATE PUBLICATION
test_sub=# ALTER SUBSCRIPTION s4 REFRESH PUBLICATION;
ALTER SUBSCRIPTION

Выполните на публикаторе те же операции вставки, что и раньше. Они реплицируются с использованием фильтра строк child (потому что publish_via_partition_root имеет значение false).

test_pub=# TRUNCATE parent;
TRUNCATE TABLE
test_pub=# INSERT INTO parent VALUES (2), (4), (6);
INSERT 0 3
test_pub=# INSERT INTO child VALUES (3), (5), (7);
INSERT 0 3

test_pub=# SELECT * FROM parent ORDER BY a;
 a
---
 2
 3
 4
 5
 6
 7
(6 rows)
test_sub=# SELECT * FROM child ORDER BY a;
 a
---
 5
 6
 7
(3 rows)


Списки столбцов

Для каждой публикации можно дополнительно указать, какие столбцы каждой таблицы реплицируются на подписчики. При этом таблица на стороне подписчика должна иметь по меньшей мере все публикуемые столбцы. Если список столбцов не задан, то реплицируются все столбцы на публикаторе. Подробную информацию по синтаксису см. на справочной странице команды CREATE PUBLICATION.

Выбор столбцов может быть обусловлен соображениями функциональности или производительности. Однако не стоит полагаться на эту функциональность в отношении безопасности: злоумышленник на стороне подписчика способен получить данные из столбцов которые не были опубликованы явно. Если речь идет о безопасности, защиту следует применять на стороне публикатора.

Если список столбцов не задан, все столбцы, добавляемые позднее, автоматически реплицируются. Это означает, что наличие списка, включающего имена всех столбцов, не равнозначно его полному отсутствию.

Список столбцов может содержать только простые ссылки на столбцы. Порядок столбцов в этом списке не сохраняется.

Указание списка столбцов, когда публикация также публикует все таблицы в схеме (FOR TABLES IN SCHEMA), не поддерживается.

Для партиционированных таблиц параметр публикации publish_via_partition_root определяет, какой список столбцов использовать. Если publish_via_partition_root имеет значение true, используется список столбцов корневой партиционированной таблицы. В противном случае, если publish_via_partition_root имеет значение false (по умолчанию), для каждой партиции используется ее собственный список столбцов.

Если публикация публикует операции UPDATE или DELETE, каждый список столбцов должен включать столбцы идентификатора реплики для этой таблицы (см. REPLICA IDENTITY). Если публикация публикует только операции INSERT, то столбцы идентификатора реплики в списке столбцов могут отсутствовать.

Списки столбцов не влияют на команду TRUNCATE.

Во время начальной синхронизации данных копируются только публикуемые столбцы. Однако если подписчик имеет версию ниже 1.5.2, то во время начальной синхронизации данных копируются все столбцы таблицы, игнорируя любые списки столбцов.

ПРЕДУПРЕЖДЕНИЕ: объединение списков столбцов из нескольких публикаций
В настоящее время отсутствует поддержка подписок, охватывающих несколько публикаций, в которых одна и та же таблица была опубликована с разными списками столбцов. Команда CREATE SUBSCRIPTION не допускает создания таких подписок, но в подобную ситуацию все равно можно попасть при добавлении или изменении списков столбцов на стороне публикации после создания подписки.

Это означает, что изменение списков столбцов таблиц в публикациях, на которые уже имеются подписки, может привести к возникновению ошибок на стороне подписчика.

Если подписка затронута этой проблемой, единственный способ возобновить репликацию — изменить один из списков столбцов на стороне публикации, чтобы они все совпадали; а затем либо заново создать подписку, либо воспользоваться ALTER SUBSCRIPTION ... DROP PUBLICATION, чтобы удалить одну из проблемных публикаций и снова ее добавить


Примеры списков столбцов

Создайте таблицу t1 для использования в следующем примере.

test_pub=# CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id));
CREATE TABLE

Создайте публикацию p1. Для таблицы t1 определяется список столбцов, призванный уменьшить количество столбцов, которые будут реплицироваться. Обратите внимание, что порядок имен столбцов в этом списке не имеет значения.

test_pub=# CREATE PUBLICATION p1 FOR TABLE t1 (id, b, a, d);
CREATE PUBLICATION

Чтобы вывести списки столбцов (если определены) для каждой публикации, можно воспользоваться утилитой psql.

test_pub=# \dRp+
                               Publication p1
  Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root
--------+------------+---------+---------+---------+-----------+----------
  qhb   | f          | t       | t       | t       | t         | f
Tables:
    "public.t1" (id, a, b, d)

Также с помощью psql можно вывести списки столбцов (если определены) для каждой таблицы.

test_pub=# \d t1
                 Table "public.t1"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           | not null |
 a      | text    |           |          |
 b      | text    |           |          |
 c      | text    |           |          |
 d      | text    |           |          |
 e      | text    |           |          |
Indexes:
    "t1_pkey" PRIMARY KEY, btree (id)
Publications:
    "p1" (id, a, b, d)

На узле-подписчике создайте таблицу t1, которой теперь требуется только подмножество столбцов, содержавшихся в таблице t1 публикатора, а также создайте подписку s1, оформленную на публикацию p1.

test_sub=# CREATE TABLE t1(id int, b text, a text, d text, PRIMARY KEY(id));
CREATE TABLE
test_sub=# CREATE SUBSCRIPTION s1
test_sub-# CONNECTION 'host=localhost dbname=test_pub application_name=s1'
test_sub-# PUBLICATION p1;
CREATE SUBSCRIPTION

На узле-публикаторе вставьте в таблицу t1 несколько строк.

test_pub=# INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2');
INSERT 0 1
test_pub=# INSERT INTO t1 VALUES(3, 'a-3', 'b-3', 'c-3', 'd-3', 'e-3');
INSERT 0 1
test_pub=# SELECT * FROM t1 ORDER BY id;
 id |  a  |  b  |  c  |  d  |  e
----+-----+-----+-----+-----+-----
  1 | a-1 | b-1 | c-1 | d-1 | e-1
  2 | a-2 | b-2 | c-2 | d-2 | e-2
  3 | a-3 | b-3 | c-3 | d-3 | e-3
(3 rows)

Реплицируются только данные из списка столбцов публикации p1.

test_sub=# SELECT * FROM t1 ORDER BY id;
 id |  b  |  a  |  d
----+-----+-----+-----
  1 | b-1 | a-1 | d-1
  2 | b-2 | a-2 | d-2
  3 | b-3 | a-3 | d-3
(3 rows)


Конфликты

Логическая репликация ведет себя подобно обычным операциям DML в том аспекте, что данные будут обновлены, даже если они были изменены локально на узле-подписчике. Если входящие данные нарушают какие-либо ограничения, репликация остановится. Это называется конфликтом. При репликации операций UPDATE или DELETE отсутствие данных не спровоцирует конфликт, и такие операции просто будут пропускаться.

Операции логической репликации выполняются с правами роли, владеющей подпиской. Отсутствие прав доступа к целевым таблицам вызовет конфликты репликации, как и включенная для целевых таблиц защита на уровне строк, применяемая к владельцу подписки, независимо от того, запретила бы какая-нибудь политика в обычной ситуации реплицируемые операции INSERT, UPDATE, DELETE или TRUNCATE. Это ограничение, касающееся защиты на уровне строк, может быть убрано в будущих версиях QHB.

Конфликт вызовет ошибку и остановит репликацию; разрешать его пользователь должен вручную. Подробную информацию о конфликте можно найти в журнале сервера подписчика.

Разрешить конфликт можно либо изменив данные на подписчике, чтобы они не конфликтовали с входящими изменениями, либо пропустив транзакцию, конфликтующую с существующими данными. Когда конфликт вызывает ошибку, репликация останавливается, а рабочий процесс логической репликации выведет в журнал сервера подписчика сообщение следующего вида:

ERROR:  duplicate key value violates unique constraint "test_pkey"
DETAIL:  Key (c)=(1) already exists.
CONTEXT:  processing remote data for replication origin "qhb_16395" during "INSERT" for replication target relation "public.test" in transaction 725 finished at 0/14C0378
-- ОШИБКА:  дублирующееся значение ключа нарушает ограничение уникальности "test_pkey"
-- ДЕТАЛИЗАЦИЯ:  Ключ (c)=(1) уже существует.
-- КОНТЕКСТ:  обработка удаленных данных для источника репликации "qhb_16395" во время операции "INSERT" для целевого отношения репликации "public.test" в транзакции 725 закончилась на 0/14C0378

LSN транзакции, содержащей изменение, которое нарушает ограничение, а также имя источника репликации можно найти в журнале сервера (в примере выше это LSN 0/14C0378 и источник репликации qhb_16395). Транзакцию, вызвавшую конфликт, можно пропустить с помощью команды ALTER SUBSCRIPTION ... SKIP с конечным LSN (т. е. LSN 0/14C0378). Конечным LSN может быть LSN, с которым транзакция фиксируется или подготавливается на публикаторе. Как вариант, транзакцию можно пропустить, вызвав функцию pg_replication_origin_advance(). Прежде чем воспользоваться этой функцией, подписку следует либо временно отключить командой ALTER SUBSCRIPTION ... DISABLE, либо использовать подписку с параметром disable_on_error. Затем можно вызвать функцию pg_replication_origin_advance() с именем_узла (т. е. qhb_16395) и LSN, следующим за конечным (т. е. 0/14C0379). Текущую позицию источников можно посмотреть в системном представлении pg_replication_origin_status. Пожалуйста, обратите внимание, что при пропуске целой транзакции пропускаются и те изменения, которые могли не нарушать никаких ограничений. Это легко может привести к несогласованности подписчика.



Ограничения

В настоящее время логическая репликация имеет следующие ограничения и недостатки функциональности. Они могут быть устранены в будущих выпусках.

  • Схема базы данных и команды DDL не реплицируются. Изначальную схему можно скопировать вручную командой qhb_dump --schema-only. Последующие изменения схемы нужно будет синхронизировать вручную. (Однако обратите внимание, что схемам необязательно быть абсолютно идентичными на обеих сторонах.) Когда определения схемы меняются в активной базе данных, логическая репликация устойчива к ошибкам. Когда схема меняется на публикаторе, и реплицированные данные начинают прибывать на сторону подписчика, но не вписываются в схему таблицы, репликация будет выдавать ошибку, пока схема не обновится. Во многих случаях случайных ошибок можно избежать, применяя дополняющие изменения схемы сначала на подписчике.

  • Данные последовательностей не реплицируются. Данные в столбцах serial или столбцах идентификации, выдаваемые последовательностями, разумеется, будут реплицированы как часть таблицы, но сама последовательность на подписчике все равно будет демонстрировать исходное значение. Если подписчик используется как база данных только для чтения, обычно это не представляет проблемы. Однако если предполагается, что на базу-подписчик будут переключаться в работе или при отказе, то последовательности нужно обновить до текущих значений, либо скопировав текущие данные из публикатора (вероятно, с помощью qhb_dump), либо выбрав достаточно большое значение из самих таблиц.

  • Репликация команд TRUNCATE поддерживается, но при опустошении групп таблиц, соединенных внешними ключами, следует соблюдать осторожность. При репликации операции опустошения подписчик опустошит ту же группу таблиц, которая была опустошена на публикаторе (явным указанием или неявно собрав их посредством CASCADE), за исключением таблиц, не входящих в подписку. Это сработает корректно, если все затронутые таблицы входят в одну подписку. Но если у некоторых опустошаемых таблиц на подписчике имеются связи по внешнему ключу с таблицами, не входящими в ту же (или любую другую) подписку, то применение операции опустошения на подписчике завершится ошибкой.

  • Большие объекты (см. главу Большие объекты) не реплицируются. Это никак нельзя обойти, кроме как хранить данные в обычных таблицах.

  • Репликация поддерживается только для таблиц, включая партиционированные. Попытки реплицировать другие типы отношений, например представления, материализованные представления или сторонние таблицы, приведут к ошибке.

  • При репликации между партиционированными таблицами по умолчанию фактическая репликация производится из листовых партиций на публикаторе, поэтому партиции с публикатора должны существовать и на подписчике, в виде допустимых целевых таблиц. (Они тоже могут быть листовыми партициями, могут дальше разбиваться на партиции или даже могут быть независимыми таблицами.) В публикациях также может быть указано, что изменения будут реплицироваться с использованием идентификаторов и схемы корневой партиционированной таблицы, а не отдельных листовых партиций, в которых фактически произошли изменения (см. справочную страницу команды CREATE PUBLICATION).



Архитектура

Логическая репликация начинается с копирования снимка данных в базе-публикаторе. После этого изменения на публикаторе передаются подписчику тогда, когда они происходят, в реальном времени. Подписчик применяет данные в том порядке, в котором на публикаторе происходят фиксации, поэтому для публикаций в рамках одной подписки гарантируется транзакционная согласованность.

Логическая репликация собирается с архитектурой, схожей с физической потоковой репликацией (см. подраздел Потоковая репликация). Это реализуется процессами «walsender» (передача WAL) и «apply» (применение). Процесс walsender запускает логическое декодирование (описанное в главе Логическое декодирование) WAL и загружает стандартный плагин логического декодирования (pgoutput). Этот плагин преобразует изменения, считанные из WAL, в протокол логической репликации (см. раздел Протокол логической потоковой репликации) и отфильтровывает данные согласно спецификации публикации. Затем данные последовательно передаются по протоколу потоковой репликации рабочему процессу apply, которые сопоставляет их с локальными таблицами и применяет отдельные изменения по мере их поступления, в правильном транзакционном порядке.

Процесс apply в базе-подписчике всегда выполняется с параметром session_replication_role, установленным в значение replica. Это означает, что по умолчанию триггеры и правила не будут запускаться на подписчике. При желании пользователи могут включить триггеры и правила для таблицы, выполнив команду ALTER TABLE с предложениями ENABLE TRIGGER и ENABLE RULE.

В настоящее время процесс применения логической репликации запускает только триггеры уровня строк, но не триггеры уровня операторов. Однако начальная синхронизация таблицы реализуется как команда COPY и поэтому запускает для INSERT как триггеры уровня строк, так и триггеры уровня операторов.


Начальный снимок

Мгновенное состояние исходных данных существующих таблиц в подписке фиксируется в снимке, а затем они копируются в параллельном экземпляре процесса apply особого вида. Этот процесс создаст собственный слот репликации и скопирует существующие данные. Как только копирование завершится, содержимое таблицы станет видимым для других серверных процессов. Когда существующие данные будут скопированы, рабочий процесс перейдет в режим синхронизации, чтобы таблица пришла в синхронизированное состояние с основным процессом apply путем потоковой передачи всех изменений, произошедших во время начального копирования данных, используя стандартную логическую репликацию. Во время этой фазы синхронизации изменения применяются и фиксируются в том же порядке, в котором они произошли на публикаторе. По завершении синхронизации управление репликацией таблицы передается обратно основному процессу apply, в котором репликация продолжатся в обычном режиме.

Примечание
Параметр публикации publish влияет только на то, какие операции DML будут реплицироваться. Он не учитывается при начальной синхронизации данных, когда происходит копирование существующих данных таблиц.



Мониторинг

Поскольку логическая репликация основана на архитектуре, схожей с физической потоковой репликацией, мониторинг узла публикации похож на мониторинг основного сервера физической репликации (см. подраздел Мониторинг).

Данные мониторинга о подписке можно увидеть в pg_stat_subscription. Это представление содержит по одной строке для каждого рабочего процесса подписки. В зависимости от состояния подписки, у нее может быть ноль или более активных рабочих процессов.

Обычно для включенной подписки выполняется один процесс apply. Для выключенной или поврежденной подписки в этом представлении будет содержаться ноль строк. Если для каких-либо таблиц выполняется начальная синхронизация данных, будут отображаться дополнительные рабочие процессы для синхронизируемых таблиц.



Безопасность

Пользователь, имеющий возможность модифицировать схему таблиц на стороне подписчика, может выполнить произвольный код как суперпользователь. Назначайте владение и право TRIGGER для таких таблиц только тем ролям, которым доверяют суперпользователи. Более того, если недоверенные пользователи могут создавать таблицы, используйте только те публикации, где явно перечислен список таблиц. Иначе говоря, создавайте подписку FOR ALL TABLES (для всех таблиц), только когда суперпользователи доверяют всем пользователям, которым разрешено создавать не временные таблицы на публикаторе или подписчике.

Роль, используемая для подключения репликации, должна иметь атрибут REPLICATION (или быть суперпользователем). Если у роли нет атрибутов SUPERUSER и BYPASSRLS, на публикаторе могут выполняться политики защиты строк. Если эта роль не доверяет всем владельцам таблиц, добавьте в строку подключения options= -crow_security=off; если впоследствии владелец таблицы добавит политику защиты строк, такое значение параметра остановит репликацию, но не даст политике выполниться. Доступ для этой роли следует сконфигурировать в qhb_hba.conf, и у нее должен быть атрибут LOGIN.

Чтобы иметь возможность скопировать исходные данные таблицы, роль, используемая для подключения репликации, должна иметь право SELECT для публикуемой таблицы (или быть суперпользователем).

Чтобы создать публикацию, пользователь должен иметь право CREATE в базе данных.

Чтобы добавлять в публикацию таблицы, пользователь должен иметь права владельца для этих таблиц. Добавлять в публикацию все таблицы в схеме, могут только суперпользователи. Создавать публикации, автоматически публикующие все таблицы или все таблицы в схеме, могут только суперпользователи.

Создавать подписки могут только суперпользователи.

Процесс apply подписки будет выполняться в локальной базе данных с правами владельца этой подписки.

На публикаторе права проверяются только один раз при запуске подключения репликации и не перепроверяются при чтении каждой записи изменения.

На подписчике права владельца подписки перепроверяются для каждой транзакции при ее применении. Если рабочий процесс выполняет применение транзакции, когда в параллельной транзакции меняется владелец подписки, применение текущей транзакции продолжится с правами старого владельца.



Параметры конфигурации

Для логической репликации нужно установить несколько параметров конфигурации.

На стороне публикатора следует установить для параметра wal_level значение logical, а для параметра max_replication_slots — число не меньше ожидаемого количества подключающихся подписок плюс некоторый резерв для синхронизации таблиц. А для параметра max_wal_senders следует установить число не меньше max_replication_slots плюс количество физических реплик, подключающихся одновременно.

На стороне подписчика тоже необходимо установить параметр max_replication_slots. В нем следует задать как минимум количество подписок, которые будут добавлены подписчику, плюс некоторый резерв для синхронизации таблиц. В параметре max_logical_replication_workers нужно задать как минимум количество подписок и, опять же, плюс некоторый резерв для синхронизации таблиц. Кроме того, может потребоваться скорректировать параметр max_worker_processes, чтобы это число включало рабочие процессы репликации, как минимум (max_logical_replication_workers + 1). Обратите внимание, что некоторые расширения и параллельные запросы тоже занимают слоты рабочих процессов из числа max_worker_processes.



Быстрая настройка

Сначала установите параметры конфигурации в qhb.conf:

wal_level = logical

Другие необходимые параметры по умолчанию имеют значения, достаточные для базовой настройки.

Файл qhb_hba.conf нужно скорректировать, чтобы разрешить репликацию (здесь значения будут зависеть от фактической конфигурации вашей сети и пользователя, от имени которого вы желаете подключаться):

host     all     repuser     0.0.0.0/0     md5

Затем в базе-публикаторе выполните:

CREATE PUBLICATION mypub FOR TABLE users, departments;

А в базе-подписчике выполните:

CREATE SUBSCRIPTION mysub CONNECTION 'dbname=foo host=bar user=repuser' PUBLICATION mypub;

Вышеуказанная команда запустит процесс репликации, который сначала синхронизирует исходное содержимое таблиц users и departments, а затем начнет реплицировать в эти таблицы инкрементальные изменения.