Uma década de replicação lógica no Postgres não elimina o projeto de chaves distribuídas
Dez anos depois do Postgres 10, cada release fechou uma lacuna que antes exigia Slony, Londiste ou pglogical. Mas quem arquiteta dados ainda decide sozinho como evitar colisão de ID, conflito silencioso e transação travada.

O Postgres↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → 10, lançado em 2017, foi a primeira versão com replicação lógica nativa. O Postgres 19, ainda em beta, é a décima release com o recurso. Nesse intervalo, cada versão pegou um pedaço do que antes só se resolvia com Londiste, Slony ou a extensão pglogical e transformou em uma linha de SQL do core. É esse percurso que Dimitri Fontaine reconstrói em um post publicado no Planet PostgreSQL, primeiro de uma série sobre casos de uso de replicação lógica, tratando a pergunta não como DBA, mas como quem projeta arquitetura de aplicação: quais topologias dá para montar só com Postgres puro, hoje, e onde ainda falta alguma coisa.
Para quem desenha sistemas distribuídos no Brasil, sobretudo em multi-tenant e sistemas de billing, a linha do tempo importa menos do que o que ela deixou de fora. E é justamente aí, nas duas lacunas que a tabela de Fontaine marca sem número de release (resolução de conflito e replicação de DDL), que mora o trabalho que ainda cabe à equipe de dados.
O que virou core, release a release
A reconstrução histórica é direta: replicar tabelas entre servidores é Postgres 10; replicar TRUNCATE é 11; publicar tabelas particionadas e assinar nelas é 13; fazer streaming de uma transação grande antes do commit é 14; filtrar linhas e colunas por publicação, além de pular uma transação problemática com ALTER SUBSCRIPTION ... SKIP, é 15; evitar loop em topologias bidirecionais (origin = none) e aplicar em paralelo é 16; manter slots e assinaturas sobrevivendo a pg_upgrade e a failover é 17; contar conflitos por tipo em pg_stat_subscription_stats é 18; replicar sequências (em beta) é 19.
Duas linhas dessa tabela não têm número de release nenhum: resolução de conflito (last update wins e variantes) e replicação de DDL. Nenhuma das duas está no core até o Postgres 19. Continuam sendo terreno do pglogical, da EDB Postgres Distributed (sucessora do BDR) ou de script próprio. É a lacuna que decide se uma arquitetura pode ser só Postgres nativo ou ainda precisa de extensão paga ou mantida à parte.
O caso de uso: hub e workers
Fontaine usa um sistema de medição (metering) como exemplo: um hub guarda plans, prices e customers; cada worker fica responsável por um subconjunto de clientes e grava os eventos de uso localmente. Dado de referência desce do hub para os workers; eventos de uso sobem dos workers para o hub, que os concentra numa tabela particionada por worker_id para fechar a fatura.
A descida de dado de referência é o caso mais simples, resolvido desde o Postgres 10 com CREATE PUBLICATION e CREATE SUBSCRIPTION. Mas publicar tudo para todo mundo expõe colunas que não deveriam sair do hub, como uma tabela de notas financeiras. O Postgres 15 resolveu isso com filtro de linha e lista de colunas por publicação:
create publication ref_w1 for table plans, prices,
customers (customer_id, name, worker_id, plan_id)
where (worker_id = 1);Aqui está a armadilha que o texto de Fontaine isola bem: o filtro usa worker_id, coluna que não é chave primária, e a chave primária é a identidade de réplica padrão. A publicação é criada sem erro. O primeiro UPDATE na tabela falha, qualquer que seja a coluna alterada, com Column used in the publication WHERE expression is not part of the replica identity. A correção é criar um índice único que inclua a coluna do filtro e declará-lo como identidade de réplica:
create unique index customers_rid on customers (customer_id, worker_id);
alter table customers replica identity using index customers_rid;Para quem modela dados, essa é a lição central: filtro de publicação não é cosmético, ele muda a exigência de índice da tabela, e a mudança não é retroativa. Trocar o filtro depois não limpa o que já foi copiado para um worker; a faxina é manual.
O problema que a versão nenhuma resolve: IDs distribuídos
O Postgres 13 tornou trivial publicar tabelas particionadas e assinar nelas, o que permite ao hub receber eventos de vários workers numa única tabela particionada por worker_id. Mas se a chave primária for só event_id, o primeiro evento do worker 2 colide com o primeiro evento do worker 1. O apply worker daquela assinatura para de vez, com conflict=insert_exists, e fica tentando de novo a cada wal_retrieve_retry_interval sem nunca avançar, enquanto as outras assinaturas seguem normalmente.
A saída estrutural é chave composta, (worker_id, event_id), que exige coordenação zero mas encarece toda tabela e toda foreign key que aponta para ela. Fontaine também descreve duas alternativas de ID único: sequência com módulo e offset por worker (clássica, funciona em qualquer versão, mas exige planejar de antemão quantos workers a frota terá) e uuidv7(), disponível desde o Postgres 18, que gera um UUID com os primeiros 48 bits como timestamp em milissegundos. A vantagem é não precisar de registro de quem é dono de qual faixa, e o índice cresce pela borda como o de uma sequência, porque o UUID é ordenável no tempo. O custo é 16 bytes em vez de 8.
Vale marcar o que o Postgres 19 não resolve aqui: sequências replicadas (ainda em beta) fazem o valor viajar do publicador para os assinantes, é o sentido contrário do que este cenário precisa, que é worker mintando IDs sem colidir com outro worker. Alocação de faixas ao estilo BDR (o que a EDB Postgres Distributed chama de galloc) segue fora do core; as patch sets de método de acesso de sequência datam de 2015 e 2016 e uma nova proposta estava em discussão na lista de e-mail no fim de 2025, mas não constava, segundo o texto, na árvore do Postgres 19.
Lotes grandes e o resto da operação
O streaming de transações grandes antes do commit (Postgres 14) e a aplicação paralela (Postgres 16) mudam a percepção de atraso quando um worker insere um lote de centenas de milhares de linhas numa única transação: sem streaming, o hub só começa a aplicar depois do commit; com aplicação paralela, que desde o Postgres 18 é o padrão para assinaturas novas, o worker de aplicação já existe antes do commit e as linhas aparecem pouco depois dele.
Na operação do dia a dia, o detalhe que decide um deploy tranquilo ou um incidente é o bloqueio ao anexar uma partição nova: create table ... partition of toma ACCESS EXCLUSIVE na tabela mãe, o que trava os apply workers já em execução; attach partition sozinho toma apenas SHARE UPDATE EXCLUSIVE. A receita segura é criar a tabela solta, com a check constraint que bate com o bound da partição, depois anexar, e só então assinar.
Contadores de conflito: o que grita e o que fica quieto
O Postgres 18 acrescentou uma coluna por tipo de conflito em pg_stat_subscription_stats. O ponto mais importante do texto é a divisão em duas famílias. update_missing, delete_missing, update_origin_differs e delete_origin_differs não param a replicação e não incrementam apply_error_count: o dado já diverge silenciosamente entre os dois lados, sem alarme nenhum. Já insert_exists, update_exists e multiple_unique_conflicts param o apply worker e ficam tentando de novo a cada intervalo de retry até alguém corrigir a linha local. E ALTER SUBSCRIPTION ... SKIP, ferramenta do Postgres 15, descarta a transação inteira, não só a linha em conflito, o que pode deixar as duas pontas em desacordo até reparo manual.
O que muda para quem projeta arquitetura de dados
A leitura de Fontaine sustenta uma conclusão que interessa a quem constrói sistemas multi-tenant, filas de eventos ou billing distribuído no Brasil: topologias hub-e-workers com replicação de dado de referência e coleta de eventos já se montam com Postgres puro, sem Londiste, sem Slony, sem trigger manual. Mas duas decisões continuam sendo do time de dados, e nenhuma release tira essa responsabilidade: o desenho da chave que evita colisão entre nós (composta ou UUIDv7, não sequência simples sem headroom) e o monitoramento ativo dos contadores de conflito silenciosos, porque pg_stat_subscription_stats não avisa sozinho quando dois servidores escrevem a mesma linha. Onde o caso de uso exige replicar DDL ou resolver conflito automaticamente (last update wins), o core ainda não chega lá, e a escolha honesta continua sendo pglogical ou EDB Postgres Distributed, não um roteiro só com SQL padrão.
Fonte: Planet PostgreSQL
Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.
PostgreSQL 19 corrige mensagens de erro em chinês paradas há sete anos
Ruohang Feng reescreveu do zero a localização chinesa do PostgreSQL, revelando erros de tradução que há décadas confundiam debugging de produção.












