Integrações, cargas de dados e consumidores de eventos frequentemente precisam executar uma operação simples de descrever:

Se o registro ainda não existe, insira. Se já existe, atualize.

Esse comportamento é conhecido como upsert, combinação de update e insert.

No PostgreSQL, ele pode ser implementado com INSERT ... ON CONFLICT.

O problema

Considere uma tabela que registra o estado mais recente de cada pipeline em uma data de referência:

CREATE TABLE pipeline_status (
    pipeline       text NOT NULL,
    reference_date date NOT NULL,
    status         text NOT NULL,
    attempts       integer NOT NULL DEFAULT 1,
    updated_at     timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT pipeline_status_uk
        UNIQUE (pipeline, reference_date)
);

Uma mesma execução pode ser informada novamente por retry, reprocessamento ou entrega duplicada de mensagem.

Um INSERT comum falharia quando a combinação de pipeline e reference_date já estivesse cadastrada:

INSERT INTO pipeline_status (
    pipeline,
    reference_date,
    status
)
VALUES (
    'customers_daily',
    DATE '2026-08-06',
    'running'
);

Consultar antes e decidir na aplicação se devemos inserir ou atualizar parece uma solução, mas cria uma janela de concorrência. Duas transações podem concluir ao mesmo tempo que a linha não existe.

Ignorando duplicidades com DO NOTHING

Quando repetir o registro não exige nenhuma mudança, podemos ignorar o conflito:

INSERT INTO pipeline_status (
    pipeline,
    reference_date,
    status
)
VALUES (
    'customers_daily',
    DATE '2026-08-06',
    'running'
)
ON CONFLICT (pipeline, reference_date)
DO NOTHING;

O PostgreSQL usa a restrição de unicidade para identificar o conflito. Se a linha já existir, o comando termina sem inserir uma nova cópia.

Essa forma é adequada quando a primeira gravação deve prevalecer.

Atualizando com DO UPDATE

Se o novo evento representa um estado mais recente, podemos atualizar a linha existente:

Neste exemplo, partimos da premissa de que os eventos chegam na ordem correta ou de que o produtor garante que o estado recebido é mais recente. Se houver possibilidade de eventos fora de ordem, o modelo precisa armazenar um timestamp ou número de sequência do evento, e o DO UPDATE deve comparar esse valor antes de substituir o estado atual.

INSERT INTO pipeline_status (
    pipeline,
    reference_date,
    status
)
VALUES (
    'customers_daily',
    DATE '2026-08-06',
    'success'
)
ON CONFLICT (pipeline, reference_date)
DO UPDATE SET
    status = EXCLUDED.status,
    attempts = pipeline_status.attempts + 1,
    updated_at = now();

EXCLUDED representa a linha que tentamos inserir. Por isso, EXCLUDED.status contém o novo valor recebido.

Ao mesmo tempo, podemos consultar os valores atuais da linha usando o nome da tabela, como em pipeline_status.attempts.

Atualizando somente quando necessário

Nem todo conflito deve provocar uma escrita. Uma condição no DO UPDATE permite evitar atualizações sem mudança real:

INSERT INTO pipeline_status (
    pipeline,
    reference_date,
    status
)
VALUES (
    'customers_daily',
    DATE '2026-08-06',
    'success'
)
ON CONFLICT (pipeline, reference_date)
DO UPDATE SET
    status = EXCLUDED.status,
    attempts = pipeline_status.attempts + 1,
    updated_at = now()
WHERE pipeline_status.status IS DISTINCT FROM EXCLUDED.status;

IS DISTINCT FROM compara os valores de maneira previsível mesmo quando existe NULL.

Recuperando o resultado com RETURNING

ON CONFLICT pode ser combinado com RETURNING:

INSERT INTO pipeline_status (
    pipeline,
    reference_date,
    status
)
VALUES (
    'customers_daily',
    DATE '2026-08-06',
    'success'
)
ON CONFLICT (pipeline, reference_date)
DO UPDATE SET
    status = EXCLUDED.status,
    attempts = pipeline_status.attempts + 1,
    updated_at = now()
RETURNING
    pipeline,
    reference_date,
    status,
    attempts,
    updated_at;

A aplicação recebe o estado final sem precisar executar outra consulta. Esse uso complementa o primeiro artigo da série, sobre RETURNING .

Existe uma ressalva ao combinar RETURNING com a condição apresentada na seção anterior: se houver conflito, mas o WHERE do DO UPDATE for falso, nenhuma linha será atualizada e, portanto, nenhuma linha será retornada.

A restrição é parte da solução

O banco precisa saber o que constitui um conflito. Normalmente isso vem de uma chave primária, restrição UNIQUE ou índice único compatível.

Sem uma regra de unicidade correta, o upsert não identifica o conflito esperado e pode criar registros duplicados:

CONSTRAINT pipeline_status_uk
    UNIQUE (pipeline, reference_date)

A escolha das colunas deve representar a identidade do evento ou entidade no domínio, não apenas facilitar a sintaxe.

Também é possível indicar uma restrição pelo nome:

ON CONFLICT ON CONSTRAINT pipeline_status_uk
DO UPDATE SET status = EXCLUDED.status

Cuidados importantes

Não transforme todo conflito em atualização

Uma violação de unicidade pode revelar um erro de modelagem ou um dado inesperado. Use DO UPDATE apenas quando o conflito fizer parte do fluxo normal.

Defina quais valores podem mudar

Evite substituir todas as colunas sem necessidade. Identificadores, datas de criação e valores imutáveis normalmente não devem ser regravados.

Reprocessamentos exigem uma identidade estável

Reconhecer uma repetição depende de uma identidade estável. Se cada retry gerar uma chave diferente, ON CONFLICT não reconhecerá a duplicidade.

Isso não torna todo upsert idempotente por si só. Neste exemplo, cada DO UPDATE executado incrementa attempts e altera updated_at; portanto, repetir o comando produz um novo estado. Quando a operação precisar ser realmente idempotente, a ação executada no conflito também deve produzir o mesmo resultado em todas as repetições.

Observe triggers e volume de escrita

Um DO UPDATE executa uma atualização real. Isso pode acionar triggers, gerar novas versões de linha e aumentar WAL. A condição WHERE ajuda quando eventos repetidos não alteram o estado.

Quando usar

INSERT ... ON CONFLICT é especialmente útil em:

  • sincronização entre sistemas;
  • carga incremental;
  • processamento de eventos com retry;
  • atualização de configurações;
  • manutenção de snapshots e estados atuais;
  • APIs que recebem uma chave idempotente.

O recurso concentra a decisão no banco e elimina a separação insegura entre “consultar” e “depois gravar”.

Referência