Soms komen we tijdens onze run als DBA's ten minste één tabel tegen die is geladen met dubbele records. Zelfs als de tabel een primaire sleutel heeft (in de meeste gevallen een automatisch oplopende sleutel), kunnen de rest van de velden dubbele waarden hebben.
SQL Server biedt echter vele manieren om van die dubbele records af te komen (bijv. met behulp van CTE's, SQL Rank-functie, subquery's met Group By, enz.).
Ik herinner me een keer, tijdens een interview, dat mij werd gevraagd hoe dubbele records in een tabel te verwijderen terwijl er slechts 1 van elk overbleef. Op dat moment kon ik niet antwoorden, maar ik was erg nieuwsgierig. Na wat onderzoek heb ik genoeg opties gevonden om dit probleem op te lossen.
Nu, jaren later, ben ik hier om u een opgeslagen procedure te presenteren die tot doel heeft de vraag te beantwoorden "hoe dubbele records in de SQL-tabel te verwijderen?". Elke DBA kan het gewoon gebruiken om wat huishoudelijk werk te doen zonder zich al te veel zorgen te maken.
Opgeslagen procedure maken:eerste overwegingen
Het account dat u gebruikt, moet voldoende rechten hebben om een Opgeslagen Procedure aan te maken in de beoogde database.
Het account dat deze opgeslagen procedure uitvoert, moet voldoende bevoegdheden hebben om de bewerkingen SELECT en DELETE uit te voeren op de doeldatabasetabel.
Deze opgeslagen procedure is bedoeld voor databasetabellen waarvoor geen primaire sleutel (noch een UNIEKE beperking) is gedefinieerd. Als uw tabel echter een primaire sleutel heeft, houdt de opgeslagen procedure geen rekening met die velden. Het zal het opzoeken en verwijderen uitvoeren op basis van de rest van de velden (gebruik het dus heel voorzichtig in dit geval).
Opgeslagen procedure gebruiken in SQL
Kopieer en plak de SP T-SQL-code die beschikbaar is in dit artikel. De SP verwacht 3 parameters:
@schemaName – de naam van het databasetabelschema, indien van toepassing. Zo niet – gebruik dbo .
@tableName – de naam van de databasetabel waarin de dubbele waarden zijn opgeslagen.
@displayOnly – indien ingesteld op 1 , de daadwerkelijke dubbele records worden niet verwijderd , maar alleen in plaats daarvan weergegeven (indien aanwezig). Standaard is deze waarde ingesteld op 0 wat betekent dat de daadwerkelijke verwijdering zal plaatsvinden als er duplicaten zijn.
SQL Server Opgeslagen Procedure Uitvoeringstests
Om de opgeslagen procedure te demonstreren, heb ik twee verschillende tabellen gemaakt - een zonder een primaire sleutel en een met een primaire sleutel. Ik heb een aantal dummy records in deze tabellen opgenomen. Laten we eens kijken welke resultaten ik krijg voor/na het uitvoeren van de Opgeslagen Procedure.
SQL-tabel met primaire sleutel
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
SQL-opgeslagen procedure Voorbeeldrecords
INSERT INTO test VALUES('A','A',1),('A','B',1),('A','C',1),('B','A',2),('B','B',3),('B','C',4)
Opgeslagen procedure uitvoeren met alleen weergave
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 1
Aangezien kolom1 en kolom2 de primaire sleutel vormen, worden de duplicaten vergeleken met de niet-primaire sleutelkolommen, in dit geval kolom3. Het resultaat is correct.
Opgeslagen procedure uitvoeren zonder alleen weergave
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 0
De dubbele records zijn verdwenen.
U moet echter voorzichtig zijn met deze benadering, omdat de eerste keer dat het record voorkomt, het record wordt verbroken. Dus als u om welke reden dan ook een heel specifiek record wilt verwijderen, moet u uw specifieke geval afzonderlijk behandelen.
SQL Tabel zonder primaire sleutel
CREATE TABLE [dbo].[duplicates](
[column1] [varchar](16) NOT NULL,
[column2] [varchar](16) NOT NULL,
[column3] [varchar](16) NOT NULL
) ON [PRIMARY]
GO
SQL-opgeslagen procedure Voorbeeldrecords
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')
Opgeslagen procedure uitvoeren met alleen weergave
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 1
De output is correct, dat zijn de dubbele records in de tabel.
Opgeslagen procedure uitvoeren zonder alleen weergave
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 0
De Opgeslagen Procedure heeft gewerkt zoals verwacht en de duplicaten zijn met succes opgeschoond.
Speciale gevallen voor deze opgeslagen procedure in SQL
Als het schema of de tabel die u opgeeft niet in uw database bestaat, zal de Opgeslagen Procedure u hiervan op de hoogte stellen en zal het script de uitvoering beëindigen.
Als u de naam van het schema leeg laat, zal het script u hiervan op de hoogte stellen en de uitvoering beëindigen.
Als u de tabelnaam leeg laat, zal het script u hiervan op de hoogte stellen en de uitvoering beëindigen.
Als u de Opgeslagen procedure uitvoert op een tabel die geen duplicaten heeft en de @displayOnly-bit activeert , krijgt u een lege resultatenset.
Opgeslagen SQL Server-procedure:volledige code
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
Conclusie
Als u niet weet hoe u dubbele records in de SQL-tabel moet verwijderen, dan zullen dergelijke tools nuttig voor u zijn. Elke DBA kan controleren of er databasetabellen zijn die geen primaire sleutels (noch unieke beperkingen) hebben, die in de loop van de tijd een stapel onnodige records kunnen ophopen (mogelijk opslagruimte verspillend). Sluit gewoon de Opgeslagen procedure aan en speel af, en u kunt aan de slag.
Je kunt een beetje verder gaan en een waarschuwingsmechanisme bouwen om je op de hoogte te stellen als er duplicaten zijn voor een specifieke tabel (na natuurlijk een beetje automatisering te hebben geïmplementeerd met behulp van deze tool), wat best handig is.
Zoals met alles met betrekking tot DBA-taken, moet u ervoor zorgen dat u altijd alles in een sandbox-omgeving test voordat u de trekker overhaalt in productie. En als je dat doet, zorg er dan voor dat je een back-up hebt van de tafel waarop je focust.