Borrar duplicados en SQL
Estaba revisando hilos viejos en Reddit —como uno hace cuando quiere ver cómo piensa la comunidad— y me encontré con un DELETE que parecía correcto a primera vista. Un usuario mostraba una consulta para borrar correos duplicados en SQL dejando el id mínimo, y la respuesta que recibió tenía un detalle que pocos notaron y que podía terminar borrando más de lo debido.
En SQLenVivo hacemos lo de siempre: reproducimos el caso, lo ejecutamos, lo observamos y lo corregimos. Nada de asumir ni de confiar ciegamente en un snippet de internet. Seguidamente explico qué hacía exactamente ese DELETE, por qué funcionaba de manera incorrecta y cómo reescribirlo para que realmente cumpla con lo que promete: borrar correos duplicados en SQL dejando solo el registro con el id mínimo por email.
Creación de la tabla de prueba
Para este ejemplo trabajaremos con una tabla real y no con un CTE. La razón es simple: cuando ejecutamos un DELETE, necesitamos persistencia. Un CTE es efímero: vive solo durante la consulta y no permite observar el antes y el después. En cambio, una tabla nos deja insertar datos, ejecutar el DELETE, revisar el resultado y confirmar qué ocurrió.
create table T (
id int identity(1,1) primary key,
email varchar(100)
);
go
Insertar datos con correos repetidos y únicos
insert into T (email) values
('a@sqlenvivo.com'),
('b@sqlenvivo.com'),
('a@sqlenvivo.com'),
('c@sqlenvivo.com'),
('b@sqlenvivo.com'),
('d@sqlenvivo.com');
go
El DELETE sospechoso del foro usado para borrar duplicados en SQL.
delete from t
where id not in (
select min(id)
from t
group by email
having count(1) > 1
);
Ese HAVING count(1) > 1 parece inocente, pero introduce una condición que cambia el comportamiento del DELETE. Al filtrar los correos que aparecen más de una vez, la subconsulta excluye todos los correos únicos. Y aquí es donde el problema se vuelve evidente: si un correo aparece una sola vez, su id no estará en el NOT IN. En consecuencia, el DELETE lo eliminará.
Este enfoque solo funciona cuando la tabla contiene exclusivamente correos repetidos; si solo hay elementos únicos, la consulta elimina registros que deberían conservarse. Este es el tipo de detalle que pasa desapercibido en un hilo de Reddit, y que en producción puede convertirse en un desastre.
No es lo que queremos.
Borrar duplicados en SQL: El DELETE correcto usando GROUP BY
delete from t
where id not in (
select min(id)
from t
group by email
);
Esto sí cumple el enunciado
- Cada correo, esté repetido o no, aporta su
min(id) - Solo se borran los duplicados
- Se conserva un solo registro por email
Borrar duplicados en SQL: La versión moderna con ROW_NUMBER()
with d as (
select
id,
row_number() over (partition by email order by id) as rn
from t
)
delete from d
where rn > 1;
Aquí no hay dudas: rn = 1 se queda y rn > 1 se va. El comportamiento es el que esperamos. Nada de interpretaciones ambiguas ni efectos colaterales: este patrón es explícito, legible y robusto; por esto el uso de funciones de ventana es la forma moderna de optimizar consultas en SQL.
Con esto cerramos el artículo: vimos el problema, lo reproducimos, lo analizamos y lo corregimos con dos enfoques distintos.
Si quieres revisar el código, la solución está disponible en el repositorio, con comentarios en inglés y en español para que puedas seguir cada paso. También te invito a pasar por este artículo donde explico los problemas de trabajar con valores NULL y cómo afecta a las consultas.
Por fortuna, en este problema de eliminación, el enunciado indicaba que los id no podían ser NULL.
Imagen de Geri Cleveland – Pixabay

