O Cenário
O job de carga do Data Warehouse falha às 3h da manhã, no meio do processamento do fato de vendas. Parte das linhas já entrou, parte ainda não. O procedimento documentado costuma ser simples: rodar de novo pela manhã.
Porém, a pergunta que quase ninguém faz é o que acontece quando alguém executa esse “rodar de novo”. Na maioria dos ambientes, ninguém sabe a resposta, porque ninguém nunca testou.
Normalmente, cargas idempotentes em Data Warehouse são projetadas para lidar justamente com esse tipo de cenário: execuções repetidas, falhas e reprocessamentos sem alterar o resultado final. Entretanto, muitas equipes ainda escrevem cargas de DW assumindo que elas vão rodar uma vez por dia, uma única vez, do começo ao fim, sem falhar. No entanto, essa premissa raramente se sustenta. Falhas de rede, timeouts na origem, reprocessamento de uma janela antiga, backfills de correção ou alguém executando o pacote manualmente para conferir um número fazem a carga rodar mais de uma vez sobre o mesmo período.
Quando a equipe não constrói a carga para isso, o resultado é duplicidade silenciosa. Ou, pior ainda, dados faltando sem ninguém perceber.
O Que É Idempotência
Uma carga é idempotente quando o resultado depende apenas da entrada, e não de quantas vezes ela foi executada. Nas cargas idempotentes em Data Warehouse, isso significa que rodar uma vez ou rodar cinco vezes sobre a mesma janela produz exatamente o mesmo estado na tabela de destino.
Apesar de parecer simples, esse é um conceito fundamental e a base de tudo o que vem depois: retry automático, backfill, recuperação após falha, reprocessamento de correção. Sem esse comportamento previsível, cada uma dessas operações vira uma aposta.
As Quatro Formas de Fazer uma Carga Idempotente
Full reload.
TRUNCATE seguido de INSERT. Por definição, é idempotente e não exige nenhum cuidado adicional. Funciona bem em dimensões e em fatos pequenos. Porém, deixa de ser uma boa alternativa quando o volume cresce e a janela de manutenção já não comporta mais o reprocessamento da tabela inteira.
Delete-insert por janela.
É a abordagem mais subestimada e resolve a maior parte dos casos. Em vez disso, você não tenta descobrir o que já foi inserido: apaga a janela e reescreve.

Dessa forma, reprocessar passa a ser reexecutar a mesma janela. Não importa se a execução anterior terminou, falhou pela metade ou nunca começou.
Além disso, repare no uso de >= e < no filtro de data, e não BETWEEN. O BETWEEN com DATETIME inclui o limite superior e cria sobreposição entre janelas consecutivas. Por esse motivo, o custo dessa abordagem é ter um índice adequado sobre a coluna de data, sem o qual o DELETE fica caro.
MERGE por chave de negócio.
Funciona, mas com duas condições que precisam ser verdadeiras: a chave de negócio tem que ser estável ao longo do tempo, e a origem tem que entregar um snapshot completo do que mudou. Se a origem entrega um delta parcial, o MERGE não tem como saber o que foi excluído lá.
Append-only com deduplicação na leitura.
Você insere tudo sempre, com um identificador de execução, e resolve a duplicidade na consulta ou em uma view, mantendo apenas a última versão de cada chave. Dessa forma, passa o custo do processamento para a leitura. Ainda assim, é uma opção válida quando a carga precisa ser rápida e o volume de leitura é baixo.
O Watermark É Onde o Problema Realmente Mora
Quase toda carga incremental guarda uma marca do último ponto processado, normalmente a maior DataAlteracao que veio da origem. É aqui que aparecem os defeitos mais difíceis de encontrar, porque nenhum deles gera erro.
O comparador.
Usar > faz você perder qualquer linha que tenha chegado exatamente no mesmo instante do watermark. Usar >= reprocessa essas linhas. A resposta correta não é ficar ajustando o operador para achar o ponto ideal. É usar >= e garantir que a carga seja idempotente, para que o reprocessamento não cause dano.
A ordem de gravação.
Gravar o novo watermark antes de confirmar a transação de carga é um erro comum:

Nesse cenário, se a carga falhar depois disso, o watermark já avançou. Aquele intervalo de dados nunca mais será lido. Além disso, não há erro, não há alerta, e a diferença só aparece meses depois, quando alguém compara um relatório com o sistema de origem.
Por isso, o UPDATE do controle precisa estar dentro da mesma transação da carga e somente depois dela.
O relógio errado.
Filtrar a DataAlteracao da origem usando GETDATE() do servidor de destino funciona até o dia em que os dois relógios divergem, ou em que a origem está em outro fuso. O limite superior da janela deve vir da origem, não do destino.
A linha que muda sem avisar.
Nem toda alteração na origem atualiza a coluna de DataAlteracao. Por exemplo, um UPDATE em massa feito direto no banco, uma trigger desabilitada ou uma correção manual. Nesse caso, essa linha simplesmente nunca vai entrar no filtro do watermark.
A única defesa prática é a janela de lookback, ou seja, reprocessar sempre alguns dias além do watermark:
![]()
E aqui está o ponto central deste artigo: o lookback só é seguro se a carga for idempotente. Se ela não for, reprocessar sete dias significa duplicar sete dias. É neste momento que a idempotência deixa de ser uma boa prática e passa a ser um requisito.
O Erro de MERGE Que Denuncia a Origem
Se você já usou MERGE, provavelmente já viu esta mensagem:
| The MERGE statement attempted to UPDATE or DELETE the same row more than once.
A tradução prática é que existem chaves duplicadas no seu staging, e você não sabia disso. A origem entregou dois registros para a mesma chave de negócio.
A solução que aparece na primeira busca é deduplicar com ROW_NUMBER() e seguir em frente. Funciona, mas esconde o problema. Antes de aplicar o paliativo, vale entender por que a duplicidade existe:

Se a duplicidade for esperada pelo desenho da origem, então deduplique com critério explícito, definindo qual registro vence. Por outro lado, se ela não for esperada, a carga deveria falhar e alertar, e não escolher uma linha em silêncio. Dessa forma, uma carga que mascara um problema na origem faz o dado errado chegar ao relatório com a mesma cara do dado certo.
Staging, Validação e Publicação
Uma carga que escreve direto na tabela final não tem onde validar. Quando o erro aparece, ele já está publicado e visível no Power BI.
Separar a carga em três etapas resolve isso. Primeiro os dados chegam ao staging. Depois as validações rodam sobre o staging. Só então a publicação acontece na tabela final.
As checagens que valem a pena rodar entre uma etapa e outra:
- Chave duplicada. Contagem de chaves de negócio repetidas no staging.
- Órfão de dimensão. Linhas do fato cuja chave estrangeira não existe na dimensão. A decisão de falhar ou de direcionar para um membro desconhecido precisa ser consciente e documentada, não acidental.
- Variação de volume. Comparação da contagem de linhas com a média das últimas execuções. Uma queda de 90% raramente é uma queda real de vendas. Normalmente é alguém que alterou um filtro na origem sem avisar.
- A maior data presente no staging comparada com a data esperada. Detecta o caso em que a carga rodou com sucesso sobre um arquivo antigo.
- Nulos inesperados. Colunas que nunca deveriam estar nulas e estão.
Nenhuma dessas checagens é complexa. Todas são um SELECT com COUNT. O que muda é que elas rodam antes da publicação, e não depois da reclamação.
O Teste de Cinco Minutos
Existe uma forma simples de descobrir se a sua carga é idempotente. Escolha uma janela já processada, tire a assinatura da tabela, rode a carga de novo sobre a mesma janela, e compare.

Em seguida, rode a consulta, execute a carga e rode a consulta novamente. Se algum dos três valores mudou, a carga não é idempotente.
O CHECKSUM_AGG não é uma função criptográfica e pode colidir em casos raros. Ainda assim, para esse propósito ele é suficiente, e a contagem de linhas junto com a soma do valor cobre a maior parte dos cenários.
Esse teste leva cinco minutos e quase nunca é feito.
Conclusão
Idempotência não é um refinamento técnico opcional. Em cargas idempotentes em Data Warehouse, ela é o que transforma o “roda de novo” de uma aposta em um procedimento previsível.
Uma carga idempotente pode ser reexecutada por um retry automático, reprocessada em backfill, ampliada com uma janela de lookback e recuperada depois de uma falha, sempre com o mesmo resultado. Uma carga que não é idempotente transforma cada uma dessas situações em uma investigação manual, geralmente feita sob pressão e com o relatório já errado na mão do gestor.
O custo de construir a carga assim é baixo, e quase todo ele está na fase de desenho. O custo de descobrir que ela não é idempotente costuma ser pago em outro lugar, meses depois, quando alguém pergunta por que o número do sistema não bate com o número do relatório.
#datawarehouse #ETL #sqlserver #tsql #engenhariadedados #qualidadededados #bancodedados #dataengineering
Por Caio Henrique Maia Brito