A veces, durante nuestra ejecución como DBA, nos encontramos con al menos una tabla que está cargada con registros duplicados. Incluso si la tabla tiene una clave principal (autoincremental en la mayoría de los casos), el resto de los campos pueden tener valores duplicados.
Sin embargo, SQL Server permite muchas maneras de deshacerse de esos registros duplicados (por ejemplo, usando CTE, la función de clasificación de SQL, subconsultas con Agrupar por, etc.).
Recuerdo una vez, durante una entrevista, me preguntaron cómo eliminar registros duplicados en una tabla dejando solo 1 de cada uno. En ese momento, no pude responder, pero tenía mucha curiosidad. Después de investigar un poco, encontré muchas opciones para resolver este problema.
Ahora, años después, estoy aquí para presentarles un procedimiento almacenado que tiene como objetivo responder a la pregunta "¿cómo eliminar registros duplicados en la tabla SQL?". Cualquier DBA puede usarlo simplemente para hacer algunas tareas domésticas sin preocuparse demasiado.
Crear procedimiento almacenado:consideraciones iniciales
La cuenta que utilice debe tener suficientes privilegios para crear un procedimiento almacenado en la base de datos prevista.
La cuenta que ejecuta este procedimiento almacenado debe tener suficientes privilegios para realizar las operaciones SELECCIONAR y ELIMINAR en la tabla de la base de datos de destino.
Este procedimiento almacenado está destinado a las tablas de la base de datos que no tienen una clave principal (ni una restricción ÚNICA) definida. Sin embargo, si su tabla tiene una clave principal, el procedimiento almacenado no tendrá en cuenta esos campos. Realizará la búsqueda y la eliminación en función del resto de los campos (así que utilícelo con mucho cuidado en este caso).
Cómo usar el procedimiento almacenado en SQL
Copie y pegue el código SP T-SQL disponible en este artículo. El SP espera 3 parámetros:
@schemaName – el nombre del esquema de la tabla de la base de datos, si corresponde. Si no, use dbo .
@nombreDeLaTabla – el nombre de la tabla de la base de datos donde se almacenan los valores duplicados.
@displayOnly – si se establece en 1 , los registros duplicados reales no se eliminarán , pero solo se muestra en su lugar (si corresponde). De forma predeterminada, este valor se establece en 0 lo que significa que la eliminación real ocurrirá si existen duplicados.
Procedimiento almacenado de SQL Server Pruebas de ejecución
Para demostrar el procedimiento almacenado, he creado dos tablas diferentes:una sin clave principal y otra con clave principal. He insertado algunos registros ficticios en estas tablas. Veamos qué resultados obtengo antes/después de ejecutar el procedimiento almacenado.
Tabla SQL con clave principal
CREATE TABLE [dbo].[test](
[column1] [varchar](16) NOT NULL,
[column2] [varchar](16) NOT NULL,
[column3] [varchar](16) NOT NULL,
CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED
(
[column1] ASC,
[column2] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
Procedimiento almacenado de SQL Registros de ejemplo
INSERT INTO test VALUES('A','A',1),('A','B',1),('A','C',1),('B','A',2),('B','B',3),('B','C',4)

Ejecutar procedimiento almacenado solo con visualización
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 1

Dado que la columna 1 y la columna 2 forman la clave principal, los duplicados se evalúan con las columnas que no son la clave principal, en este caso, la columna 3. El resultado es correcto.
Ejecutar procedimiento almacenado sin visualización solamente
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 0

Los registros duplicados se han ido.
Sin embargo, debe tener cuidado con este enfoque porque la primera aparición del registro es la que se cortará. Por lo tanto, si por alguna razón necesita que se elimine un registro muy específico, entonces debe abordar su caso particular por separado.
SQL Tabla sin clave principal
CREATE TABLE [dbo].[duplicates](
[column1] [varchar](16) NOT NULL,
[column2] [varchar](16) NOT NULL,
[column3] [varchar](16) NOT NULL
) ON [PRIMARY]
GO
Procedimiento almacenado de SQL Registros de ejemplo
INSERT INTO duplicates VALUES
('John','Smith','Y'),
('John','Smith','Y'),
('John','Smith','N'),
('Peter','Parker','N'),
('Bruce','Wayne','Y'),
('Steve','Rogers','Y'),
('Steve','Rogers','Y'),
('Tony','Stark','N')

Ejecutar procedimiento almacenado solo con visualización
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 1

El resultado es correcto, esos son los registros duplicados en la tabla.
Ejecutar procedimiento almacenado sin visualización solamente
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 0

El procedimiento almacenado funcionó como se esperaba y los duplicados se limpiaron con éxito.
Casos especiales para este procedimiento almacenado en SQL
Si el esquema o la tabla que está especificando no existe dentro de su base de datos, el procedimiento almacenado le notificará y el script finalizará su ejecución.

Si deja el nombre del esquema en blanco, el script le notificará y finalizará su ejecución.

Si deja el nombre de la tabla en blanco, el script le notificará y finalizará su ejecución.

Si ejecuta el procedimiento almacenado en una tabla que no tiene duplicados y activa el bit @displayOnly , obtendrá un conjunto de resultados vacío.

Procedimiento almacenado de SQL Server:código completo
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author : Alejandro Cobar
-- Create date: 2021-06-01
-- Description: SP to delete duplicate rows in a table
-- =============================================
CREATE PROCEDURE DBA_DeleteDuplicates
@schemaName VARCHAR(128),
@tableName VARCHAR(128),
@displayOnly BIT = 0
AS
BEGIN
SET NOCOUNT ON;
IF LEN(@schemaName) = 0
BEGIN
PRINT 'You must specify the schema of the table!'
RETURN
END
IF LEN(@tableName) = 0
BEGIN
PRINT 'You must specify the name of the table!'
RETURN
END
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName)
BEGIN
DECLARE @pkColumnName VARCHAR(128);
DECLARE @columnName VARCHAR(128);
DECLARE @sqlCommand VARCHAR(MAX);
DECLARE @columnsList VARCHAR(MAX);
DECLARE @pkColumnsList VARCHAR(MAX);
DECLARE @pkColumns TABLE(pkColumn VARCHAR(128));
DECLARE @limit INT;
INSERT INTO @pkColumns
SELECT K.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS C
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K ON C.TABLE_NAME = K.TABLE_NAME AND C.CONSTRAINT_SCHEMA = K.CONSTRAINT_SCHEMA
WHERE C.CONSTRAINT_TYPE = 'PRIMARY KEY'
AND C.CONSTRAINT_SCHEMA = @schemaName AND C.TABLE_NAME = @tableName
IF((SELECT COUNT(*) FROM @pkColumns) > 0)
BEGIN
DECLARE pk_cursor CURSOR FOR
SELECT * FROM @pkColumns
OPEN pk_cursor
FETCH NEXT FROM pk_cursor INTO @pkColumnName
WHILE @@FETCH_STATUS = 0
BEGIN
SET @pkColumnsList = CONCAT(@pkColumnsList,'',@pkColumnName,',')
FETCH NEXT FROM pk_cursor INTO @pkColumnName
END
CLOSE pk_cursor
DEALLOCATE pk_cursor
SET @pkColumnsList = SUBSTRING(@pkColumnsList,1,LEN(@pkColumnsList)-1)
END
DECLARE columns_cursor CURSOR FOR
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName AND COLUMN_NAME NOT IN (SELECT pkColumn FROM @pkColumns)
ORDER BY ORDINAL_POSITION;
OPEN columns_cursor
FETCH NEXT FROM columns_cursor INTO @columnName
WHILE @@FETCH_STATUS = 0
BEGIN
SET @columnsList = CONCAT(@columnsList,'',@columnName,',')
FETCH NEXT FROM columns_cursor INTO @columnName
END
CLOSE columns_cursor
DEALLOCATE columns_cursor
SET @columnsList = SUBSTRING(@columnsList,1,LEN(@columnsList)-1)
IF((SELECT COUNT(*) FROM @pkColumns) > 0)
BEGIN
IF(CHARINDEX(',',@columnsList) = 0)
SET @limit = LEN(@columnsList)+1
ELSE
SET @limit = CHARINDEX(',',@columnsList)
SET @sqlCommand = CONCAT('WITH CTE (',@columnsList,',DuplicateCount',')
AS (SELECT ',@columnsList,',',
'ROW_NUMBER() OVER(PARTITION BY ',@columnsList,' ',
'ORDER BY ',SUBSTRING(@columnsList,1,@limit-1),') AS DuplicateCount
FROM [',@schemaName,'].[',@tableName,'])
')
IF @displayOnly = 0
SET @sqlCommand = CONCAT(@sqlCommand,'DELETE FROM CTE WHERE DuplicateCount > 1;')
IF @displayOnly = 1
SET @sqlCommand = CONCAT(@sqlCommand,'SELECT ',@columnsList,',MAX(DuplicateCount) AS DuplicateCount FROM CTE WHERE DuplicateCount > 1 GROUP BY ',@columnsList)
END
ELSE
BEGIN
SET @sqlCommand = CONCAT('WITH CTE (',@columnsList,',DuplicateCount',')
AS (SELECT ',@columnsList,',',
'ROW_NUMBER() OVER(PARTITION BY ',@columnsList,' ',
'ORDER BY ',SUBSTRING(@columnsList,1,CHARINDEX(',',@columnsList)-1),') AS DuplicateCount
FROM [',@schemaName,'].[',@tableName,'])
')
IF @displayOnly = 0
SET @sqlCommand = CONCAT(@sqlCommand,'DELETE FROM CTE WHERE DuplicateCount > 1;')
IF @displayOnly = 1
SET @sqlCommand = CONCAT(@sqlCommand,'SELECT * FROM CTE WHERE DuplicateCount > 1;')
END
EXEC (@sqlCommand)
END
ELSE
BEGIN
PRINT 'Table doesn't exist within this database!'
RETURN
END
END
GO
Conclusión
Si no sabe cómo eliminar registros duplicados en una tabla SQL, herramientas como esta le serán útiles. Cualquier DBA puede verificar si hay tablas de base de datos que no tienen claves principales (ni restricciones únicas) para ellas, que podrían acumular una pila de registros innecesarios con el tiempo (potencialmente desperdiciando almacenamiento). Simplemente conecte y reproduzca el procedimiento almacenado y estará listo para comenzar.
Puede ir un poco más allá y crear un mecanismo de alerta que le notifique si hay duplicados para una tabla específica (después de implementar un poco de automatización con esta herramienta, por supuesto), lo que resulta bastante útil.
Al igual que con cualquier cosa relacionada con las tareas de DBA, asegúrese de probar siempre todo en un entorno de espacio aislado antes de apretar el gatillo en producción. Y cuando lo haga, asegúrese de tener una copia de seguridad de la tabla en la que se enfoca.