Às vezes, durante nossa execução como DBAs, encontramos pelo menos uma tabela carregada com registros duplicados. Mesmo que a tabela tenha uma Chave Primária (uma de incremento automático na maioria dos casos), o restante dos campos pode ter valores duplicados.
No entanto, o SQL Server permite muitas maneiras de se livrar desses registros duplicados (por exemplo, usando CTEs, função SQL Rank, subconsultas com Group By, etc.).
Lembro que uma vez, durante uma entrevista, me perguntaram como excluir registros duplicados em uma tabela deixando apenas 1 de cada. Naquela época, eu não era capaz de responder, mas eu estava muito curioso. Depois de pesquisar um pouco, encontrei muitas opções para resolver esse problema.
Agora, anos depois, estou aqui para apresentar a vocês um Stored Procedure que visa responder a pergunta “como excluir registros duplicados em uma tabela SQL?”. Qualquer DBA pode simplesmente usá-lo para fazer algumas tarefas domésticas sem se preocupar muito.
Criar procedimento armazenado:considerações iniciais
A conta que você usa deve ter privilégios suficientes para criar um procedimento armazenado no banco de dados pretendido.
A conta que executa este procedimento armazenado deve ter privilégios suficientes para executar as operações SELECT e DELETE na tabela de banco de dados de destino.
Este procedimento armazenado destina-se às tabelas de banco de dados que não possuem uma chave primária (nem uma restrição UNIQUE) definida. No entanto, se sua tabela tiver uma chave primária, o procedimento armazenado não levará esses campos em consideração. Ele executará a pesquisa e a exclusão com base no restante dos campos (portanto, use-o com muito cuidado neste caso).
Como usar o procedimento armazenado em SQL
Copie e cole o código SP T-SQL disponível neste artigo. O SP espera 3 parâmetros:
@schemaName – o nome do esquema da tabela de banco de dados, se aplicável. Se não – use dbo .
@tableName – o nome da tabela de banco de dados onde os valores duplicados são armazenados.
@displayOnly – se definido como 1 , os registros duplicados reais não serão excluídos , mas apenas exibido em vez disso (se houver). Por padrão, esse valor é definido como 0 significando que a exclusão real acontecerá se houver duplicatas.
Procedimento armazenado do SQL Server Testes de execução
Para demonstrar o procedimento armazenado, criei duas tabelas diferentes – uma sem chave primária e outra com chave primária. Eu inseri alguns registros fictícios nessas tabelas. Vamos verificar quais resultados obtenho antes/depois de executar o procedimento armazenado.
Tabela SQL com chave primária
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
Procedimento armazenado SQL Registros de exemplo
INSERT INTO test VALUES('A','A',1),('A','B',1),('A','C',1),('B','A',2),('B','B',3),('B','C',4)
Executar procedimento armazenado apenas com exibição
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 1
Como a coluna1 e a coluna2 formam a chave primária, as duplicatas são avaliadas em relação às colunas que não são da chave primária, neste caso, coluna3. O resultado está correto.
Executar procedimento armazenado sem exibição apenas
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 0
Os registros duplicados sumiram.
No entanto, você deve ter cuidado com essa abordagem, pois a primeira ocorrência do registro é aquela que será cortada. Portanto, se por algum motivo você precisar que um registro muito específico seja excluído, deverá abordar seu caso específico separadamente.
SQL Tabela sem chave primária
CREATE TABLE [dbo].[duplicates](
[column1] [varchar](16) NOT NULL,
[column2] [varchar](16) NOT NULL,
[column3] [varchar](16) NOT NULL
) ON [PRIMARY]
GO
Procedimento armazenado SQL Registros de exemplo
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')
Executar procedimento armazenado apenas com exibição
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 1
A saída está correta, esses são os registros duplicados na tabela.
Executar procedimento armazenado sem exibição apenas
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 0
O procedimento armazenado funcionou conforme o esperado e as duplicatas foram limpas com êxito.
Casos especiais para este procedimento armazenado em SQL
Se o esquema ou tabela que você está especificando não existir em seu banco de dados, o procedimento armazenado o notificará e o script encerrará sua execução.
Se você deixar o nome do esquema em branco, o script o notificará e encerrará sua execução.
Se você deixar o nome da tabela em branco, o script o notificará e encerrará sua execução.
Se você executar o procedimento armazenado em uma tabela que não possui duplicatas e ativar o bit @displayOnly , você obterá um conjunto de resultados vazio.
Procedimento armazenado do 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
Conclusão
Se você não sabe como excluir registros duplicados na tabela SQL, ferramentas como essa serão úteis para você. Qualquer DBA pode verificar se existem tabelas de banco de dados que não possuem Chaves Primárias (nem restrições exclusivas) para elas, que podem acumular uma pilha de registros desnecessários ao longo do tempo (potencialmente desperdiçando armazenamento). Basta conectar e reproduzir o procedimento armazenado e pronto.
Você pode ir um pouco além e construir um mecanismo de alerta para notificá-lo se houver duplicatas para uma tabela específica (depois de implementar um pouco de automação usando essa ferramenta, é claro), o que é bastante útil.
Como com qualquer coisa relacionada a tarefas de DBA, certifique-se de sempre testar tudo em um ambiente de sandbox antes de puxar o gatilho na produção. E quando o fizer, certifique-se de ter um backup da tabela em que você se concentra.