Lição 12.5 · Tempo de leitura: ~7 min
As linhas duplicadas aparecem em quase todos os bancos de dados reais: uma importação corre duas vezes, um formulário é enviado duas vezes, uma restrição de unicidade nunca foi criada. "Encontrar duplicados" e "remover duplicados mantendo um" são tarefas clássicas de entrevista de SQL. Nesta lição, você vai encontrar valores duplicados com GROUP BY ... HAVING, listar cada cópia com uma função de janela, remover com segurança as cópias a mais no MySQL e evitar novos duplicados com uma restrição.
Encontrar e Remover Linhas Duplicadas em SQL
Primeiro decida o que é um duplicado. As linhas raramente são idênticas em todas as colunas: normalmente têm ids e datas de criação diferentes. Um duplicado significa a mesma chave de negócio: o mesmo e-mail, o mesmo nome e data de nascimento, o mesmo número de encomenda. Todas as consultas abaixo agrupam por essa chave.
Os exemplos usam a sintaxe do MySQL 8.0 e criam a sua própria tabela, por isso execute-os no playground num banco sem dados de exemplo.
Dados de Exemplo
CREATE TABLE contact (
id INT PRIMARY KEY,
email VARCHAR(100) NOT NULL,
full_name VARCHAR(50) NOT NULL
);
INSERT INTO contact VALUES
(1, 'anna@mail.com', 'Anna Lee'),
(2, 'bob@mail.com', 'Bob Stone'),
(3, 'anna@mail.com', 'Anna Lee'),
(4, 'carl@mail.com', 'Carl Diaz'),
(5, 'bob@mail.com', 'Bob Stone'),
(6, 'Anna@Mail.com', 'Anna Lee'),
(7, 'dina@mail.com', 'Dina Park');
Passo 1: Encontrar Valores Duplicados
Agrupe pela chave e mantenha os grupos com mais de uma linha:
SELECT email, COUNT(*) AS copies
FROM contact
GROUP BY email
HAVING COUNT(*) > 1;
Resultado: anna@mail.com com 3 cópias e bob@mail.com com 2.
Repare que Anna@Mail.com foi contado junto com anna@mail.com. A collation padrão do MySQL compara strings sem distinguir maiúsculas de minúsculas, por isso são o mesmo valor. O PostgreSQL distingue maiúsculas e encontraria só 2 cópias de Anna; aí, agrupe por LOWER(email) para obter a mesma resposta. Para uma chave de várias colunas, liste-as todas: GROUP BY first_name, last_name.
Passo 2: Ver Cada Linha Duplicada
O GROUP BY mostra uma linha por valor. Para ver as próprias linhas duplicadas, com os seus ids, conte as cópias com uma função de janela e mantenha as linhas cuja contagem é superior a um:
WITH counted AS (
SELECT id, email, full_name,
COUNT(*) OVER (PARTITION BY email) AS copies
FROM contact
)
SELECT id, email, full_name, copies
FROM counted
WHERE copies > 1
ORDER BY email, id;
Resultado: 5 linhas: ids 1, 3, 6 para Anna e 2, 5 para Bob.
Antes de remover qualquer coisa, olhe para estas linhas: as cópias significam realmente o mesmo, e qual delas deve ficar?
Passo 3: Marcar as Cópias a Remover
Numere as linhas dentro de cada grupo com ROW_NUMBER(). A linha com o número 1 é a que fica; aqui, a mais antiga, com o menor id:
SELECT id, email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM contact
ORDER BY email, id;
Resultado: os ids 1, 2, 4, 7 recebem rn = 1; os ids 3, 5, 6 recebem 2 ou 3 e são as cópias a mais.
O ORDER BY dentro da janela decide que linha sobrevive: ORDER BY id DESC mantém a mais recente, ORDER BY updated_at DESC a atualizada mais recentemente.
Passo 4: Remover as Cópias a Mais
No MySQL, uma subconsulta num DELETE não pode ler a tabela de onde se remove (erro 1093). Envolva-a numa tabela derivada, que o MySQL materializa primeiro:
DELETE FROM contact
WHERE id IN (
SELECT id
FROM (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM contact
) AS numbered
WHERE rn > 1
);
Resultado: 3 linhas removidas; ficam os ids 1, 2, 4 e 7, um por e-mail.
Uma forma mais antiga, exclusiva do MySQL, usa um self-join: remover cada linha para a qual existe uma linha com o mesmo e-mail e um id menor:
DELETE c
FROM contact c
JOIN contact k ON k.email = c.email AND k.id < c.id;
Resultado: as mesmas 3 linhas removidas.
No PostgreSQL, a forma habitual é DELETE FROM contact c USING contact k WHERE k.email = c.email AND k.id < c.id;; no SQL Server, pode remover diretamente a partir de uma CTE com ROW_NUMBER().
Execute o SELECT do passo 3 antes do DELETE, compare o número de linhas afetadas e, em produção, faça a remoção dentro de uma transação.
Passo 5: Evitar Novos Duplicados
A limpeza só ajuda até à próxima importação. Uma restrição de unicidade faz o banco de dados rejeitar duplicados:
ALTER TABLE contact ADD CONSTRAINT uq_contact_email UNIQUE (email);
Acrescente-a depois da limpeza: enquanto houver duplicados, a criação da restrição falha. As importações podem então usar INSERT IGNORE ou INSERT ... ON DUPLICATE KEY UPDATE (Lição 8.1) para saltar ou atualizar as linhas existentes.
Prática
Principais conclusões:
- Um duplicado é uma chave de negócio repetida, não necessariamente uma linha idêntica; agrupe por essa chave.
GROUP BY key HAVING COUNT(*) > 1encontra valores duplicados;COUNT(*) OVER (PARTITION BY key)mostra cada linha duplicada.ROW_NUMBER() OVER (PARTITION BY key ORDER BY ...)marca a linha a manter (rn = 1) e as cópias a remover.- No MySQL, remova através de uma tabela derivada ou de um self-join para evitar o erro 1093.
- As maiúsculas e a collation decidem o que é igual; termine com uma restrição
UNIQUE.
Perguntas Frequentes
Como encontrar linhas duplicadas em SQL?
Agrupe pelas colunas que deviam ser únicas e filtre os grupos: SELECT email, COUNT(*) FROM contact GROUP BY email HAVING COUNT(*) > 1;. Para ver as linhas completas, use COUNT(*) OVER (PARTITION BY email) e mantenha as linhas em que é maior do que 1.
Como remover duplicados mantendo uma linha?
Numere as linhas de cada grupo com ROW_NUMBER() OVER (PARTITION BY key ORDER BY id) e remova as linhas com número maior do que 1. No MySQL, coloque a numeração numa tabela derivada dentro do DELETE, ou use um self-join: DELETE c FROM t c JOIN t k ON k.key = c.key AND k.id < c.id.
Por que o MySQL mostra o erro 1093 quando removo duplicados?
O MySQL não permite que uma subconsulta num DELETE ou UPDATE leia diretamente a mesma tabela. Envolver a subconsulta numa tabela derivada (SELECT id FROM (...) AS d) faz o MySQL materializá-la primeiro, e o erro desaparece.
Como encontrar duplicados em várias colunas?
Liste-as todas no GROUP BY e no PARTITION BY: GROUP BY first_name, last_name HAVING COUNT(*) > 1. As linhas só são duplicadas quando todas as colunas listadas coincidem.