(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisboa
ETL

Idempotência em ETL: como re-executar sem duplicar dados

João Barros 04 de July de 2026 5 min de leitura

Um pipeline ETL idempotente é aquele que podes correr duas, cinco ou dez vezes seguidas e obténs sempre o mesmo resultado final, sem linhas duplicadas nem dados corrompidos. Esta propriedade é o que te deixa dormir descansado: quando um processo falha a meio e precisa de ser reprocessado, a idempotência garante que a segunda tentativa não estraga o que a primeira já tinha carregado. Em produção, falhas de rede, timeouts e reinícios acontecem, e um pipeline idempotente transforma esses sustos numa simples nova execução. A seguir vês, passo a passo, como transformar um Load "append cego" num Load idempotente com uma chave de negócio e um MERGE.

Pré-requisitos

  • Um destino relacional (por exemplo SQL Server, Azure SQL ou PostgreSQL) onde crias a tabela final.
  • Noções básicas de SQL: INSERT, UPDATE e, idealmente, MERGE.
  • Uma origem com um identificador estável por registo (por exemplo, o id da venda).

Passo 1: Perceber porque um Load simples não é idempotente

O padrão mais comum na fase de Load é um INSERT direto da staging para o destino. O problema é que cada execução acrescenta linhas: se o pipeline falhar depois de carregar metade das vendas e voltares a correr, as linhas já carregadas entram outra vez e ficas com registos repetidos. Imagina um relatório de faturação a contar a mesma venda duas vezes: o erro propaga-se a toda a análise. Um Load idempotente depende apenas do que está na origem, e não de quantas vezes correu.

Idempotência em ETL: como re-executar sem duplicar dados
INSERT INTO vendas_dest (id_venda, cliente, total)
SELECT id_venda, cliente, total
FROM vendas_stg;

Passo 2: Escolher uma chave de negócio estável

A chave de negócio é a coluna (ou conjunto de colunas) que identifica um registo de forma única e que não muda ao longo do tempo, como um id_venda. É esta chave que permite decidir, para cada linha, se deves inserir um registo novo ou atualizar um já existente, em vez de inserir sempre. Se precisares de mais do que uma coluna para garantir unicidade, usa uma chave composta.

Passo 3: Substituir o INSERT por um MERGE (upsert)

O MERGE compara a origem com o destino pela chave: se a linha já existe, faz UPDATE; se não existe, faz INSERT. Como é uma única instrução atómica, ou corre por completo ou não altera nada. Correr o mesmo MERGE duas vezes com a mesma origem produz exatamente o mesmo estado final, que é a definição prática de idempotência.

MERGE INTO vendas_dest AS d
USING vendas_stg AS s
   ON d.id_venda = s.id_venda
WHEN MATCHED THEN
   UPDATE SET d.cliente = s.cliente,
              d.total   = s.total
WHEN NOT MATCHED THEN
   INSERT (id_venda, cliente, total)
   VALUES (s.id_venda, s.cliente, s.total);
Dica: em PostgreSQL, o equivalente é INSERT ... ON CONFLICT ... DO UPDATE; em motores sem MERGE, o padrão delete-insert do Passo 5 funciona sempre.

Passo 4: Garantir que a origem não traz duplicados

O MERGE dá erro se a origem tiver a mesma chave em várias linhas, porque não sabe qual usar. Por isso, limpa os duplicados na staging antes do MERGE, mantendo apenas a versão mais recente de cada chave com ROW_NUMBER().

WITH ranked AS (
   SELECT id_venda, cliente, total,
          ROW_NUMBER() OVER (
             PARTITION BY id_venda
             ORDER BY data_atualizacao DESC
          ) AS rn
   FROM vendas_raw
)
INSERT INTO vendas_stg (id_venda, cliente, total)
SELECT id_venda, cliente, total
FROM ranked
WHERE rn = 1;

Passo 5: Carga por partição, uma alternativa idempotente

Quando reprocessas uma fatia inteira de dados (por exemplo, um dia), há um padrão ainda mais simples: apagar essa partição no destino e voltar a inseri-la. Dentro de uma transação, a operação fica atómica e pode ser repetida sem risco.

BEGIN TRANSACTION;

DELETE FROM vendas_dest
WHERE data_venda = '2026-07-04';

INSERT INTO vendas_dest (id_venda, cliente, total, data_venda)
SELECT id_venda, cliente, total, data_venda
FROM vendas_stg
WHERE data_venda = '2026-07-04';

COMMIT;

Verificar o resultado

A prova real é simples: corre o pipeline duas vezes seguidas e confirma que o número de linhas e os totais não mudam da primeira para a segunda execução. Depois, procura chaves repetidas no destino; se esta consulta não devolver nada, o Load é idempotente.

SELECT id_venda, COUNT(*) AS n
FROM vendas_dest
GROUP BY id_venda
HAVING COUNT(*) > 1;

Conclusão

Com uma chave de negócio, um MERGE (ou um delete-insert por partição) e uma limpeza de duplicados na staging, o teu Load passa a ser seguro para repetir tantas vezes quantas precisares. A partir daqui, envolve tudo numa transação, acrescenta logging e combina a idempotência com carga incremental para pipelines rápidos e fiáveis. Da próxima vez que um pipeline falhar a meio, faz-te uma pergunta: consigo simplesmente correr outra vez sem medo? Se a resposta for sim, já tens um ETL idempotente.

Partilhar: