Para encontrar duplicatas em SQL, agrupe as linhas pelas colunas que deveriam ser únicas e mantenha os grupos com mais de uma linha. Depois, funções de janela e self-joins mostram as próprias linhas duplicadas, para você decidir qual cópia manter. Os exemplos funcionam no PostgreSQL, MySQL, SQLite e SQL Server.
Como encontrar duplicatas em SQL
GROUP BY junta as linhas com o mesmo valor, COUNT(*) conta cada grupo e HAVING mantém os grupos com mais de uma linha:
SELECT columns, COUNT(*)
FROM table_name
GROUP BY columns
HAVING COUNT(*) > 1;
Por exemplo, para encontrar endereços de email usados por mais de um cliente:
-- Input: customers
-- id | name | email
-- ---+----------------+----------------------
-- 1 | Maria Lopez | maria.lopez@gmail.com
-- 2 | Sam Patel | sam@patel.dev
-- 3 | M. Lopez | maria.lopez@gmail.com
-- 4 | Kenji Watanabe | kenji@watanabe.jp
SELECT email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
-- Output:
-- email | copies
-- ----------------------+-------
-- maria.lopez@gmail.com | 2
Para considerar linhas duplicadas apenas quando várias colunas coincidem, liste todas elas tanto no SELECT quanto no GROUP BY.
Regras importantes
-
Toda coluna selecionada precisa estar no
GROUP BYou dentro de uma agregação. Adicionarnameà consulta acima não mostra os nomes das duplicatas. Isso gera um erro, exceto no SQLite, que mostra um nome por email:
-- Input: the customers table above
SELECT name, email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
-- ERROR: column "customers.name" must appear in the GROUP BY clause or be used in an aggregate function
Para ver todas as linhas duplicadas, use uma das consultas em “Sintaxe alternativa”.
-
NULLs são agrupados juntos, e maiúsculas e minúsculas fazem diferença. Todos os emails NULL caem no mesmo grupo, então parecem duplicatas entre si, enquanto
Ana@Silva.deveana@silva.devnão:
-- Input: users
-- id | email
-- ---+--------------
-- 1 | ana@silva.dev
-- 2 | NULL
-- 3 | Ana@Silva.dev
-- 4 | NULL
SELECT email, COUNT(*) AS copies
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- Output:
-- email | copies
-- ------+-------
-- NULL | 2
-- MySQL and SQL Server ignore case by default, so they also return ana@silva.dev | 2
-- Skip the NULLs and ignore case:
SELECT LOWER(email) AS email, COUNT(*) AS copies
FROM users
WHERE email IS NOT NULL
GROUP BY LOWER(email)
HAVING COUNT(*) > 1;
-- Output:
-- email | copies
-- --------------+-------
-- ana@silva.dev | 2
Sintaxe alternativa
GROUP BY retorna uma linha por valor duplicado. Para obter as próprias linhas duplicadas, conte sobre uma janela. COUNT(*) OVER (PARTITION BY email) coloca a contagem de cada email em todas as linhas que o contêm:
-- Input: the customers table above
SELECT id, name, email, copies
FROM (
SELECT id, name, email, COUNT(*) OVER (PARTITION BY email) AS copies
FROM customers
) AS c
WHERE copies > 1
ORDER BY id;
-- Output:
-- id | name | email | copies
-- ---+-------------+-----------------------+-------
-- 1 | Maria Lopez | maria.lopez@gmail.com | 2
-- 3 | M. Lopez | maria.lopez@gmail.com | 2
EXISTS faz o mesmo sem função de janela: mantenha cada linha para a qual existe outra linha com o mesmo email:
-- Input: the customers table above
SELECT id, name, email
FROM customers c
WHERE EXISTS (
SELECT 1 FROM customers other
WHERE other.email = c.email AND other.id <> c.id
);
-- Output:
-- id | name | email
-- ---+-------------+----------------------
-- 1 | Maria Lopez | maria.lopez@gmail.com
-- 3 | M. Lopez | maria.lopez@gmail.com
Quando você deve procurar duplicatas?
Duplicatas entram por importações, cliques duplos, novas tentativas e constraints ausentes. Estas são as quatro tarefas que você mais vai precisar fazer com elas.
Verificar se uma coluna é única
Antes de adicionar uma constraint UNIQUE, ou quando um login ou uma busca começa a retornar duas linhas, liste os valores que aparecem mais de uma vez. GROUP BY com HAVING responde isso diretamente.
Ver todas as linhas envolvidas
Saber qual valor se repete não basta para corrigir: você precisa das linhas completas para ver como diferem e qual está certa. Use os valores duplicados em uma consulta na mesma tabela.
Marcar as cópias extras
Para limpar, mantenha uma linha de cada conjunto de duplicatas e remova o resto. ROW_NUMBER() numera as linhas dentro de cada conjunto, então tudo depois da linha 1 é uma cópia extra.
Detectar envios duplicados
Um botão clicado duas vezes ou um pagamento repetido deixa duas linhas iguais em tudo, menos no ID. Fazer um join da tabela com ela mesma forma os pares, e t1.id < t2.id lista cada par uma única vez.
Exemplos reais de como encontrar duplicatas
Encontrar quartos de hotel com reserva dupla
Um quarto só pode ser reservado uma vez por noite, então uma duplicata é a combinação do mesmo quarto e da mesma noite. Agrupe pelas duas colunas:
-- Input: bookings
-- booking_id | room | night | guest
-- -----------+------+------------+----------
-- 1 | 204 | 2026-11-06 | R. Okafor
-- 2 | 204 | 2026-11-07 | R. Okafor
-- 3 | 310 | 2026-11-06 | L. Chen
-- 4 | 204 | 2026-11-06 | P. Novak
SELECT room, night, COUNT(*) AS bookings
FROM bookings
GROUP BY room, night
HAVING COUNT(*) > 1;
-- Output:
-- room | night | bookings
-- -----+------------+---------
-- 204 | 2026-11-06 | 2
Listar contas que compartilham um endereço de email
As pessoas se cadastram duas vezes com o mesmo endereço em maiúsculas e minúsculas diferentes. Particione por LOWER(email) para colocar as duas contas lado a lado:
-- Input: accounts
-- id | username | email
-- ---+----------+----------------
-- 1 | jkim | jin@kim.kr
-- 2 | olivia_r | olivia@reyes.mx
-- 3 | jin.kim | Jin@Kim.kr
-- 4 | noah_b | noah@baker.uk
SELECT id, username, email, accounts_with_email
FROM (
SELECT id, username, email,
COUNT(*) OVER (PARTITION BY LOWER(email)) AS accounts_with_email
FROM accounts
) AS a
WHERE accounts_with_email > 1
ORDER BY id;
-- Output:
-- id | username | email | accounts_with_email
-- ---+----------+------------+--------------------
-- 1 | jkim | jin@kim.kr | 2
-- 3 | jin.kim | Jin@Kim.kr | 2
Sinalizar pessoas duplicadas antes de uma limpeza
Trate usuários com o mesmo nome e sobrenome como uma única pessoa, mantenha o registro mais antigo e rotule os demais. Uma CTE separa a numeração da rotulagem:
-- Input: users
-- id | first_name | last_name | email
-- ---+------------+-----------+----------------
-- 1 | Priya | Shah | priya@shah.net
-- 2 | Tom | Becker | tom@becker.de
-- 3 | Priya | Shah | pshah@work.com
-- 4 | Lena | Kowalski | lena@kowal.pl
-- 5 | Tom | Becker | tbecker@mail.de
WITH numbered AS (
SELECT id, first_name, last_name, email,
ROW_NUMBER() OVER (PARTITION BY first_name, last_name ORDER BY id) AS copy_number
FROM users
)
SELECT id, first_name, last_name, email,
CASE WHEN copy_number > 1 THEN 'Duplicate' ELSE 'Original' END AS status
FROM numbered
ORDER BY id;
-- Output:
-- id | first_name | last_name | email | status
-- ---+------------+-----------+-----------------+----------
-- 1 | Priya | Shah | priya@shah.net | Original
-- 2 | Tom | Becker | tom@becker.de | Original
-- 3 | Priya | Shah | pshah@work.com | Duplicate
-- 4 | Lena | Kowalski | lena@kowal.pl | Original
-- 5 | Tom | Becker | tbecker@mail.de | Duplicate
As linhas marcadas como Duplicate são as que devem ser mescladas ou excluídas. O guia para remover duplicatas mostra como excluí-las.
Encontrar pedidos feitos duas vezes por engano
Dois pedidos do mesmo cliente para o mesmo produto no mesmo dia provavelmente são um único pedido enviado duas vezes:
-- Input: orders
-- order_id | customer_id | product | order_date
-- ---------+-------------+-----------+-----------
-- 1001 | 42 | Desk Lamp | 2026-03-02
-- 1002 | 42 | Desk Lamp | 2026-03-02
-- 1003 | 42 | Desk Lamp | 2026-03-09
-- 1004 | 57 | Desk Lamp | 2026-03-02
SELECT o1.order_id AS first_order, o2.order_id AS repeat_order,
o1.customer_id, o1.product
FROM orders o1
JOIN orders o2
ON o1.customer_id = o2.customer_id
AND o1.product = o2.product
AND o1.order_date = o2.order_date
AND o1.order_id < o2.order_id;
-- Output:
-- first_order | repeat_order | customer_id | product
-- ------------+--------------+-------------+----------
-- 1001 | 1002 | 42 | Desk Lamp
Leituras adicionais sobre duplicatas
Neste blog:
- Guia para remover duplicatas em SQL: exclua as cópias extras depois de encontrá-las
- Detectar linhas duplicadas em uma tabela do SQL Server: o mesmo problema em T-SQL
- Guia de SQL COUNT DISTINCT: conte valores deixando as duplicatas de fora
-
Guia de SQL GROUP BY: agrupamento e
HAVINGem detalhes -
Having vs Where em SQL: por que o filtro de contagem vai no
HAVING - Criando constraints únicas no PostgreSQL: impeça a entrada de novas duplicatas
- Guia de CTE vs subconsulta em SQL: as duas formas de estruturar essas consultas
Documentação oficial:
- PostgreSQL: GROUP BY e HAVING, funções de janela
- MySQL: SELECT, incluindo GROUP BY e HAVING, ROW_NUMBER()
- SQLite: SELECT, incluindo GROUP BY e HAVING, funções de janela
- SQL Server: HAVING, ROW_NUMBER
Beekeeper Studio é um cliente de banco de dados gratuito e open source
A melhor ferramenta de consultas e edição de SQL que já usei. Tem tudo o que preciso para gerenciar meu banco de dados. - ⭐⭐⭐⭐⭐ Mit
O Beekeeper Studio é rápido, intuitivo e fácil de usar. Ele suporta vários bancos de dados e funciona muito bem no Windows, Mac e Linux.
O que os usuários dizem sobre o Beekeeper Studio
"O Beekeeper Studio substituiu completamente meu antigo fluxo de trabalho com SQL. É rápido, intuitivo e torna o trabalho com bancos de dados prazeroso de novo."
"Já testei muitas GUIs de banco de dados, mas o Beekeeper encontra o equilíbrio perfeito entre recursos e simplicidade. Simplesmente funciona."