← Voltar ao blog

Como remover registros duplicados no Firebird sem apagar o registro certo

GROUP BY encontra, ROW_NUMBER escolhe qual fica e DELETE ... EXISTS remove o resto. Veja o passo a passo seguro no Firebird, com backup e transação.


Duplicados aparecem de várias formas: uma integração que roda duas vezes, um clique repetido no botão de salvar, uma importação sem chave única. O resultado é sempre parecido — relatórios inflados, totais errados e a sensação de que "o banco está sujo".

Remover duplicados é fácil. Remover os duplicados certos é o que exige cuidado. Um DELETE mal escrito apaga histórico, quebra relacionamentos e não tem desfazer.

Este guia mostra o processo seguro: definir a chave, encontrar os grupos, escolher qual registro fica, apagar o resto dentro de uma transação e impedir que o problema volte.


Primeiro: defina o que é duplicado

Duplicado não é "linha igual em todas as colunas". Quase sempre é a repetição de uma chave de negócio:

Tabela Chave provável
CLIENTES CPF ou CNPJ
COB_CONTRATO_PARCELAS IDCONTRATO + NUMERO_PARCELA
TIMECARDS IDPROFISSIONAL + DATA + IDTAREFA
INTEGRACAO_RECEBIDA IDEXTERNO

Defina a chave antes de escrever qualquer SELECT. Sem isso, você pode "limpar" registros que são legítimos — duas parcelas com o mesmo valor, por exemplo, não são duplicadas.

Encontrar os grupos com GROUP BY

SELECT CPF, COUNT(*) AS QTD
FROM CLIENTES
WHERE CPF IS NOT NULL
GROUP BY CPF
HAVING COUNT(*) > 1
ORDER BY QTD DESC;

HAVING COUNT(*) > 1 mostra apenas as chaves repetidas. O WHERE CPF IS NOT NULL evita tratar vários registros sem CPF como duplicados entre si — no Firebird, NULL não é igual a NULL em comparações.

Para ver os registros completos de cada grupo:

SELECT c.*
FROM CLIENTES c
JOIN (
  SELECT CPF
  FROM CLIENTES
  WHERE CPF IS NOT NULL
  GROUP BY CPF
  HAVING COUNT(*) > 1
) d ON d.CPF = c.CPF
ORDER BY c.CPF, c.ID;

Antes de apagar, confira se os grupos fazem sentido. Uma amostra com FIRST 50 costuma revelar surpresas.

Escolher qual registro fica

A regra precisa ser explícita. As mais comuns:

  • menor ID — mantém o registro mais antigo;
  • maior ID — mantém o mais recente;
  • maior data de atualização — mantém o último estado;
  • registro com mais dados preenchidos — mantém o mais completo.

Com Firebird 3.0+, ROW_NUMBER resolve:

SELECT
  ID,
  CPF,
  DATA_CADASTRO,
  ROW_NUMBER() OVER (
    PARTITION BY CPF
    ORDER BY DATA_CADASTRO DESC, ID DESC
  ) AS RN
FROM CLIENTES
WHERE CPF IS NOT NULL;

RN = 1 é o registro que fica. Todos com RN > 1 são candidatos à remoção.

Se o banco ainda é 2.5, use uma subconsulta correlacionada para achar o menor ID:

SELECT c.ID, c.CPF
FROM CLIENTES c
WHERE c.ID > (
  SELECT MIN(c2.ID)
  FROM CLIENTES c2
  WHERE c2.CPF = c.CPF
);

Apagar com segurança

Opção 1: manter o menor ID

DELETE FROM CLIENTES
WHERE CPF IS NOT NULL
  AND EXISTS (
    SELECT 1
    FROM CLIENTES c2
    WHERE c2.CPF = CLIENTES.CPF
      AND c2.ID < CLIENTES.ID
  );

A lógica: apaga todo registro que tenha outro com o mesmo CPF e ID menor. Sobra apenas o menor ID de cada grupo.

Opção 2: manter o mais recente

DELETE FROM CLIENTES
WHERE CPF IS NOT NULL
  AND EXISTS (
    SELECT 1
    FROM CLIENTES c2
    WHERE c2.CPF = CLIENTES.CPF
      AND c2.ID > CLIENTES.ID
  );

Opção 3: tabela temporária com os IDs

Para volumes grandes, separe a seleção da exclusão. Assim você revisa a lista antes de apagar:

CREATE GLOBAL TEMPORARY TABLE TMP_DUP_IDS (
  ID INTEGER
);

INSERT INTO TMP_DUP_IDS (ID)
SELECT c.ID
FROM CLIENTES c
WHERE c.CPF IS NOT NULL
  AND EXISTS (
    SELECT 1 FROM CLIENTES c2
    WHERE c2.CPF = c.CPF AND c2.ID < c.ID
  );

-- conferir
SELECT COUNT(*) FROM TMP_DUP_IDS;

-- apagar
DELETE FROM CLIENTES
WHERE ID IN (SELECT ID FROM TMP_DUP_IDS);

A tabela temporária existe apenas durante a conexão e some no COMMIT. É a opção mais controlável para produção.

Transação, contagem e backup

Nunca rode uma limpeza dessas sem rede de segurança:

  1. Faça backup do banco ou da tabela.
  2. Conte antes: SELECT COUNT(*) FROM CLIENTES;
  3. Conte os candidatos: quantos IDs serão apagados.
  4. Abra a transação e execute o DELETE.
  5. Conte depois e confira se a diferença bate com os candidatos.
  6. Valide relacionamentos que apontam para os IDs removidos.
  7. COMMIT só depois da conferência; caso contrário, ROLLBACK.

No isql, use SAVEPOINT antes da exclusão para poder voltar ao ponto sem desfazer a análise:

SAVEPOINT antes_da_limpeza;
DELETE FROM ...;
-- conferir
ROLLBACK TO SAVEPOINT antes_da_limpeza; -- ou COMMIT

O caso do BLOB: DISTINCT não compara conteúdo

Se a chave de deduplicação envolve um campo BLOB (narrativa, observação, texto longo), cuidado: o DISTINCT não compara o conteúdo do BLOB como você espera. Ele pode comparar identificadores internos ou simplesmente recusar a operação — e os "duplicados" continuam aparecendo.

Converta o BLOB para texto antes de comparar:

SELECT DISTINCT
  IDCONTRATO,
  CAST(NARRATIVA AS VARCHAR(32000)) AS NARRATIVA_TXT
FROM HISTORICO;

Se o texto for maior que o limite do VARCHAR, compare um hash calculado na aplicação ou use SUBSTRING para uma amostra. O importante é nunca assumir que DISTINCT resolve BLOB.

Impedir que volte

Limpar sem criar a restrição é enxugar gelo. Depois da limpeza, crie um índice único na chave de negócio:

CREATE UNIQUE INDEX UX_CLIENTES_CPF ON CLIENTES (CPF);

Em integrações, o índice único é a única garantia real contra concorrência. Verificar "se existe" antes de inserir em PHP tem uma janela de corrida: duas execuções simultâneas podem passar pela verificação e inserir as duas.

Se o fluxo precisa atualizar quando existe e inserir quando não existe, use uma operação atômica. No Firebird 2.1+:

UPDATE OR INSERT INTO CLIENTES (CPF, NOME, DATA_CADASTRO)
VALUES (?, ?, ?)
MATCHING (CPF);

MATCHING usa a chave primária ou um índice único para decidir entre UPDATE e INSERT, dentro de uma única operação.

Passo a passo

  1. Defina a chave de negócio.
  2. Encontre os grupos com GROUP BY + HAVING.
  3. Revise uma amostra dos registros.
  4. Escolha a regra de qual fica (menor ID, mais recente etc.).
  5. Gere a lista de IDs a remover.
  6. Faça backup e conte antes/depois.
  7. Execute dentro de transação e valide relacionamentos.
  8. Crie o índice único para impedir recorrência.
  9. Ajuste a integração para operação atômica.

Checklist

  • A chave de duplicidade está definida e documentada.
  • Registros com NULL na chave foram tratados à parte.
  • A regra de qual registro fica é explícita.
  • A lista de IDs foi revisada antes do DELETE.
  • Existe backup e a operação está em transação.
  • Relacionamentos que apontam para os IDs foram verificados.
  • Um índice único impede novos duplicados.
  • A integração usa operação atômica, não "verifica e insere".

Conclusão

Limpar duplicados é menos sobre o DELETE e mais sobre as decisões anteriores: qual é a chave, qual registro representa a verdade e como impedir que o problema volte. Com GROUP BY, uma regra clara de escolha e transação, a limpeza deixa de ser risco e vira manutenção.

Se você precisa montar a consulta de deduplicação sem errar a sintaxe do Firebird, o Sqlify gera o SQL a partir da descrição em português, com GROUP BY, HAVING e junções explicadas. Descreva "clientes com CPF repetido" e revise a query pronta.

Leia também: