Lección 12.5 · Tiempo de lectura: ~7 min
Las filas duplicadas aparecen en casi cualquier base de datos real: una importación se ejecuta dos veces, un formulario se envía dos veces, nunca se agregó una restricción de unicidad. "Encontrar duplicados" y "eliminar duplicados conservando uno" son ejercicios clásicos de entrevista de SQL. En esta lección encontrarás valores duplicados con GROUP BY ... HAVING, verás cada copia con una función de ventana, eliminarás de forma segura las copias sobrantes en MySQL y evitarás nuevos duplicados con una restricción.
Encontrar y eliminar filas duplicadas en SQL
Primero decide qué es un duplicado. Las filas casi nunca son idénticas en todas las columnas: normalmente tienen ids y fechas de creación distintos. Un duplicado significa la misma clave de negocio: el mismo correo, el mismo nombre y fecha de nacimiento, el mismo número de pedido. Todas las consultas de abajo agrupan por esa clave.
Los ejemplos usan la sintaxis de MySQL 8.0 y crean su propia tabla, así que ejecútalos en el playground en una base sin datos de ejemplo.
Datos de ejemplo
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');
Paso 1: encontrar valores duplicados
Agrupa por la clave y conserva los grupos con más de una fila:
SELECT email, COUNT(*) AS copies
FROM contact
GROUP BY email
HAVING COUNT(*) > 1;
Resultado: anna@mail.com con 3 copias y bob@mail.com con 2.
Fíjate en que Anna@Mail.com se contó junto con anna@mail.com. La collation predeterminada de MySQL compara las cadenas sin distinguir mayúsculas de minúsculas, así que son el mismo valor. PostgreSQL sí las distingue y encontraría solo 2 copias de Anna; allí agrupa por LOWER(email) para obtener la misma respuesta. Para una clave de varias columnas, enuméralas todas: GROUP BY first_name, last_name.
Paso 2: ver cada fila duplicada
GROUP BY muestra una fila por valor. Para ver las propias filas duplicadas, con sus ids, cuenta las copias con una función de ventana y conserva las filas donde el conteo es mayor que uno:
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 filas: ids 1, 3, 6 para Anna y 2, 5 para Bob.
Antes de eliminar nada, revisa estas filas: ¿las copias significan realmente lo mismo? ¿Cuál debe quedarse?
Paso 3: marcar las copias a eliminar
Numera las filas dentro de cada grupo con ROW_NUMBER(). La fila con el número 1 es la que se conserva; aquí, la más antigua, con el id más pequeño:
SELECT id, email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM contact
ORDER BY email, id;
Resultado: los ids 1, 2, 4, 7 obtienen rn = 1; los ids 3, 5, 6 obtienen 2 o 3 y son las copias sobrantes.
El ORDER BY dentro de la ventana decide qué fila sobrevive: ORDER BY id DESC conserva la más nueva, ORDER BY updated_at DESC la actualizada más recientemente.
Paso 4: eliminar las copias sobrantes
En MySQL, una subconsulta en DELETE no puede leer la tabla de la que se elimina (error 1093). Envuélvela en una tabla derivada, que MySQL materializa primero:
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 filas eliminadas; quedan los ids 1, 2, 4 y 7, uno por correo.
Una forma más antigua, propia de MySQL, usa un self-join: eliminar cada fila para la que existe otra con el mismo correo y un id menor:
DELETE c
FROM contact c
JOIN contact k ON k.email = c.email AND k.id < c.id;
Resultado: se eliminan las mismas 3 filas.
En PostgreSQL, la forma habitual es DELETE FROM contact c USING contact k WHERE k.email = c.email AND k.id < c.id;; en SQL Server puedes eliminar directamente desde una CTE con ROW_NUMBER().
Ejecuta el SELECT del paso 3 antes del DELETE, compara la cantidad de filas afectadas y, en producción, haz el borrado dentro de una transacción.
Paso 5: evitar nuevos duplicados
La limpieza solo sirve hasta la próxima importación. Una restricción de unicidad hace que la base de datos rechace los duplicados:
ALTER TABLE contact ADD CONSTRAINT uq_contact_email UNIQUE (email);
Agrégala después de la limpieza: mientras haya duplicados, crear la restricción falla. Las importaciones pueden usar luego INSERT IGNORE o INSERT ... ON DUPLICATE KEY UPDATE (lección 8.1) para saltar o actualizar las filas existentes.
Práctica
Conclusiones clave:
- Un duplicado es una clave de negocio repetida, no necesariamente una fila idéntica; agrupa por esa clave.
GROUP BY key HAVING COUNT(*) > 1encuentra valores duplicados;COUNT(*) OVER (PARTITION BY key)muestra cada fila duplicada.ROW_NUMBER() OVER (PARTITION BY key ORDER BY ...)marca la fila a conservar (rn = 1) y las copias a eliminar.- En MySQL, elimina a través de una tabla derivada o un self-join para evitar el error 1093.
- Las mayúsculas y la collation deciden qué es igual; termina con una restricción
UNIQUE.
Preguntas frecuentes
¿Cómo encuentro filas duplicadas en SQL?
Agrupa por las columnas que deberían ser únicas y filtra los grupos: SELECT email, COUNT(*) FROM contact GROUP BY email HAVING COUNT(*) > 1;. Para ver las filas completas, usa COUNT(*) OVER (PARTITION BY email) y conserva las filas donde es mayor que 1.
¿Cómo elimino duplicados conservando una fila?
Numera las filas de cada grupo con ROW_NUMBER() OVER (PARTITION BY key ORDER BY id) y elimina las que tengan un número mayor que 1. En MySQL, pon la numeración en una tabla derivada dentro del DELETE, o usa un self-join: DELETE c FROM t c JOIN t k ON k.key = c.key AND k.id < c.id.
¿Por qué MySQL muestra el error 1093 al eliminar duplicados?
MySQL no permite que una subconsulta en DELETE o UPDATE lea directamente la misma tabla. Si envuelves la subconsulta en una tabla derivada (SELECT id FROM (...) AS d), MySQL la materializa primero y el error desaparece.
¿Cómo encuentro duplicados en varias columnas?
Enuméralas todas en GROUP BY y en PARTITION BY: GROUP BY first_name, last_name HAVING COUNT(*) > 1. Las filas son duplicadas solo cuando coinciden todas las columnas enumeradas.