Quando a base deixa de ser brinquedo e passa a atualizar todo dia, aparece uma decisão que muda a qualidade do seu dado: você vai reprocessar tudo ou atualizar só o que mudou?
O UPSERT existe para resolver isso. O problema é que muita gente pula direto para o MERGE e trata a parte difícil como detalhe. Não é detalhe. A parte difícil é decidir qual linha vale, como a origem chega e o que fazer quando ela chega errada.
Antes do UPSERT, três perguntas precisam estar respondidas
Você precisa saber:
- qual é a chave do registro;
- qual versão vence em caso de conflito;
- o que fazer com atraso, correção e ausência no lote.
Sem essas respostas, a carga incremental não está modelada. Está só empurrando linha de um lado para o outro.
O erro clássico é confiar demais na origem crua
Um dos atalhos mais perigosos é aplicar MERGE diretamente em uma tabela que já chega com duplicidade, atraso ou registro corrigido.
Se a staging traz duas versões do mesmo pedido, por exemplo, o banco pode:
- atualizar mais de uma vez;
- escolher uma versão errada;
- falhar por conflito;
- consolidar um histórico que ninguém pretendia manter.
É por isso que UPSERT ruim quase sempre nasce de staging ruim.
O fluxo mínimo que segura a carga
O desenho mais simples continua sendo o mais útil:
- origem bruta;
- staging tratada;
- tabela final.
A origem bruta guarda o lote como ele chegou. A staging resolve padronização, deduplicação e regra de recência. A tabela final recebe só o que já está pronto para ser consolidado.
Essa separação parece burocracia até o dia em que alguém precisa explicar por que um pedido voltou para o status antigo.
A regra de recência precisa ser explícita
Imagine uma chave pedido_id com dois registros no mesmo lote. Um veio às 09:00, outro às 11:00. Qual deles vale?
Se você não responder isso antes, o banco vai aceitar a ambiguidade do jeito que a sua implementação permitir. E a tabela final vai parecer correta até o momento em que alguém comparar com a operação.
Na maioria dos cenários, o caminho mais seguro é deduplicar antes do UPSERT, usando uma coluna como updated_at para manter apenas a versão mais recente por chave.
O MERGE é a etapa visível, não a proteção principal
Com a origem já tratada, a escrita fica parecida com isto:
MERGE INTO pedidos_final AS destino
USING staging_pedidos AS origem
ON destino.pedido_id = origem.pedido_id
WHEN MATCHED THEN
UPDATE SET
status = origem.status,
valor = origem.valor,
updated_at = origem.updated_at
WHEN NOT MATCHED THEN
INSERT (pedido_id, status, valor, updated_at)
VALUES (origem.pedido_id, origem.status, origem.valor, origem.updated_at);
O MERGE importa, mas ele não compensa uma origem mal resolvida. Se a staging ainda estiver ambígua, você só vai formalizar o erro com mais velocidade.
Tem um ponto cego que quase sempre fica de fora
Nem toda fonte manda delete explícito. Às vezes o registro some. Às vezes muda de status. Às vezes o lote chega parcial.
Por isso, UPSERT sozinho não responde tudo. Você ainda precisa decidir:
- ausência no lote significa exclusão;
- ausência significa atraso de ingestão;
- ausência significa que a origem mandou só um recorte.
Essa conversa não é detalhe de engenharia. Ela muda o significado do dado final.
Validação depois da carga continua obrigatória
Depois do UPSERT, confira pelo menos:
- quantidade de chaves únicas na tabela final;
- duplicidade por chave;
- quantos registros entraram;
- quantos foram atualizados;
- se alguma métrica de controle saiu do intervalo esperado.
Se a tabela final passou a ter mais de uma linha por chave, a carga falhou, mesmo que a execução tenha terminado sem erro.
Quando full refresh ainda é a melhor decisão
Tem um pragmatismo importante aqui: nem toda base precisa de UPSERT.
Se o volume é pequeno, a janela é curta e reprocessar custa pouco, full refresh ainda pode ser melhor. Menos elegante, talvez. Mais confiável, muitas vezes.
O erro não é escolher full refresh. O erro é escolher incremental só porque parece mais avançado.
O mínimo confiável
Se você quiser resumir o padrão sem floreio, ele cabe nesta sequência:
origem bruta -> staging deduplicada -> UPSERT -> validação
É simples. E justamente por isso resolve bastante coisa. Boa carga incremental não nasce de sofisticação. Nasce de critério.
