Database
 sql >> Base de Dados >  >> RDS >> Database

Procedimento armazenado para excluir registros duplicados na tabela SQL


À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.