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:
- Faça backup do banco ou da tabela.
- Conte antes:
SELECT COUNT(*) FROM CLIENTES; - Conte os candidatos: quantos IDs serão apagados.
- Abra a transação e execute o
DELETE. - Conte depois e confira se a diferença bate com os candidatos.
- Valide relacionamentos que apontam para os IDs removidos.
COMMITsó 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
- Defina a chave de negócio.
- Encontre os grupos com
GROUP BY+HAVING. - Revise uma amostra dos registros.
- Escolha a regra de qual fica (menor ID, mais recente etc.).
- Gere a lista de IDs a remover.
- Faça backup e conte antes/depois.
- Execute dentro de transação e valide relacionamentos.
- Crie o índice único para impedir recorrência.
- Ajuste a integração para operação atômica.
Checklist
- A chave de duplicidade está definida e documentada.
- Registros com
NULLna 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: