Para encontrar duplicados en SQL, agrupa las filas por las columnas que deberían ser únicas y quédate con los grupos que tengan más de una fila. Luego, las funciones de ventana y los self-joins te muestran las filas duplicadas en sí, para que decidas qué copia conservar. Los ejemplos funcionan en PostgreSQL, MySQL, SQLite y SQL Server.
Cómo encontrar duplicados en SQL
GROUP BY junta las filas con el mismo valor, COUNT(*) cuenta cada grupo y HAVING se queda con los grupos que tienen más de una fila:
SELECT columns, COUNT(*)
FROM table_name
GROUP BY columns
HAVING COUNT(*) > 1;
Por ejemplo, para encontrar direcciones de correo que usa más de un 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 filas como duplicadas solo cuando coinciden varias columnas a la vez, incluye cada una de ellas tanto en SELECT como en GROUP BY.
Reglas que debes conocer
-
Cada columna que selecciones debe estar en
GROUP BYo dentro de un agregado. Agregarnamea la consulta anterior no muestra los nombres de los duplicados. Da un error, excepto en SQLite, que muestra un nombre por correo:
-- 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 cada fila duplicada, usa una de las consultas de “Sintaxis alternativa”.
-
Los NULL se agrupan juntos y las mayúsculas importan. Todos los correos NULL caen en un mismo grupo, así que parecen duplicados entre sí, mientras que
Ana@Silva.devyana@silva.devno:
-- 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
Sintaxis alternativa
GROUP BY devuelve una fila por cada valor duplicado. Para obtener las filas duplicadas en sí, mejor cuenta sobre una ventana. COUNT(*) OVER (PARTITION BY email) pone el conteo de cada correo en todas las filas que lo tienen:
-- 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 hace lo mismo sin una función de ventana: conserva cada fila para la que existe otra fila con el mismo correo:
-- 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
¿Cuándo deberías buscar duplicados?
Los duplicados se cuelan por importaciones, doble clic, reintentos y restricciones faltantes. Estas son las cuatro tareas que más seguido vas a necesitar hacer con ellos.
Verifica que una columna sea única
Antes de agregar una restricción UNIQUE, o cuando un inicio de sesión o una búsqueda empieza a devolver dos filas, lista los valores que aparecen más de una vez. GROUP BY con HAVING responde esto directamente.
Ve todas las filas involucradas
Saber qué valor se repite no basta para arreglarlo: necesitas las filas completas para ver en qué se diferencian y cuál es la correcta. Usa los valores duplicados en otra consulta sobre la misma tabla.
Marca las copias sobrantes
Para limpiar, conserva una fila de cada grupo de duplicados y elimina el resto. ROW_NUMBER() numera las filas dentro de cada grupo, así que todo lo que venga después de la fila 1 es una copia sobrante.
Detecta envíos dobles
Un botón con doble clic o un pago reintentado deja dos filas que coinciden en todo menos en su ID. Unir la tabla consigo misma las empareja, y t1.id < t2.id lista cada par una sola vez.
Ejemplos reales para encontrar duplicados
Encuentra habitaciones de hotel reservadas dos veces
Una habitación solo puede reservarse una vez por noche, así que un duplicado es la misma habitación en la misma noche. Agrupa por ambas columnas:
-- 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
Lista las cuentas que comparten un correo
Hay gente que se registra dos veces con la misma dirección escrita con distintas mayúsculas. Particiona por LOWER(email) para poner ambas cuentas 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
Marca personas duplicadas antes de una limpieza
Trata a los usuarios con el mismo nombre y apellido como una sola persona, conserva el registro más antiguo y etiqueta los demás. Un CTE separa la numeración del etiquetado:
-- 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
Las filas marcadas como Duplicate son las que hay que fusionar o eliminar. La guía para eliminar duplicados te muestra cómo borrarlas.
Encuentra pedidos hechos dos veces por error
Dos pedidos del mismo cliente, del mismo producto y el mismo día probablemente son un solo pedido enviado dos veces:
-- 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
Más lecturas sobre duplicados
En este blog:
- Guía para eliminar duplicados en SQL: borra las copias sobrantes una vez que las encuentres
- Detecta filas duplicadas en una tabla de SQL Server: el mismo problema en T-SQL
- Guía de SQL COUNT DISTINCT: cuenta valores sin los duplicados
-
Guía de SQL GROUP BY: agrupación y
HAVINGa fondo -
Having vs Where en SQL: por qué el filtro del conteo va en
HAVING - Cómo crear restricciones únicas en PostgreSQL: evita que entren nuevos duplicados
- Guía de CTE vs subconsulta en SQL: las dos formas de estructurar estas consultas
Documentación oficial:
- PostgreSQL: GROUP BY y HAVING, funciones de ventana
- MySQL: SELECT, incluidos GROUP BY y HAVING, ROW_NUMBER()
- SQLite: SELECT, incluidos GROUP BY y HAVING, funciones de ventana
- SQL Server: HAVING, ROW_NUMBER
Beekeeper Studio es un cliente de bases de datos gratuito y de código abierto
La mejor herramienta de consultas y edición de SQL que he usado. Tiene todo lo que necesito para administrar mi base de datos. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio es rápido, intuitivo y fácil de usar. Es compatible con muchísimas bases de datos y funciona de maravilla en Windows, Mac y Linux.
Lo que dicen los usuarios de Beekeeper Studio
"Beekeeper Studio reemplazó por completo mi antiguo flujo de trabajo con SQL. Es rápido, intuitivo y hace que trabajar con bases de datos vuelva a ser agradable."
"He probado muchas GUI de bases de datos, pero Beekeeper logra el equilibrio perfecto entre funciones y simplicidad. Simplemente funciona."