Někdy během našeho běhu jako DBA narazíme na alespoň jednu tabulku, která je načtena duplicitními záznamy. I když má tabulka primární klíč (ve většině případů automaticky přírůstkový), ostatní pole mohou mít duplicitní hodnoty.
SQL Server však umožňuje mnoho způsobů, jak se těchto duplicitních záznamů zbavit (např. pomocí CTE, funkce SQL Rank, poddotazů s Group By atd.).
Pamatuji si, jak jsem se jednou během rozhovoru zeptal, jak odstranit duplicitní záznamy v tabulce a ponechat pouze 1 z každého. Tehdy jsem nebyl schopen odpovědět, ale byl jsem velmi zvědavý. Po malém zkoumání jsem našel spoustu možností, jak tento problém vyřešit.
Nyní, po letech, jsem tu, abych vám představil uloženou proceduru, jejímž cílem je odpovědět na otázku „jak odstranit duplicitní záznamy v tabulce SQL? Každý DBA jej může jednoduše použít k nějakému úklidu, aniž by se příliš staral.
Vytvoření uložené procedury:Počáteční úvahy
Účet, který používáte, musí mít dostatečná oprávnění k vytvoření uložené procedury v zamýšlené databázi.
Účet spouštějící tuto uloženou proceduru musí mít dostatečná oprávnění k provádění operací SELECT a DELETE proti cílové databázové tabulce.
Tato uložená procedura je určena pro databázové tabulky, které nemají definovaný primární klíč (ani omezení UNIQUE). Pokud však vaše tabulka obsahuje primární klíč, uložená procedura tato pole nezohlední. Provede vyhledání a odstranění na základě zbývajících polí (takže je v tomto případě používejte velmi opatrně).
Jak používat uloženou proceduru v SQL
Zkopírujte a vložte kód SP T-SQL dostupný v tomto článku. SP očekává 3 parametry:
@schemaName – název schématu databázové tabulky, pokud se použije. Pokud ne, použijte dbo .
@tableName – název databázové tabulky, kde jsou uloženy duplicitní hodnoty.
@displayOnly – pokud je nastaveno na 1 , skutečné duplicitní záznamy nebudou smazány , ale zobrazí se pouze místo toho (pokud existuje). Ve výchozím nastavení je tato hodnota nastavena na 0 což znamená, že k skutečnému smazání dojde pokud existují duplikáty.
Uložená procedura SQL Server Testy provedení
Abych demonstroval uloženou proceduru, vytvořil jsem dvě různé tabulky – jednu bez primárního klíče a jednu s primárním klíčem. Do těchto tabulek jsem vložil nějaké fiktivní záznamy. Pojďme se podívat, jaké výsledky dostanu před/po provedení uložené procedury.
Tabulka SQL s primárním klíčem
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
Uložená procedura SQL Příklady záznamů
INSERT INTO test VALUES('A','A',1),('A','B',1),('A','C',1),('B','A',2),('B','B',3),('B','C',4)
Provést uloženou proceduru pouze se zobrazením
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 1
Vzhledem k tomu, že sloupce1 a sloupec2 tvoří primární klíč, jsou duplikáty vyhodnoceny proti sloupcům jiného než primárního klíče, v tomto případě sloupce3. Výsledek je správný.
Provést uloženou proceduru pouze bez zobrazení
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 0
Duplicitní záznamy jsou pryč.
S tímto přístupem však musíte být opatrní, protože první výskyt záznamu je ten, který bude řezat. Pokud tedy z jakéhokoli důvodu potřebujete smazat velmi konkrétní záznam, musíte svůj konkrétní případ řešit samostatně.
SQL Tabulka bez primárního klíče
CREATE TABLE [dbo].[duplicates](
[column1] [varchar](16) NOT NULL,
[column2] [varchar](16) NOT NULL,
[column3] [varchar](16) NOT NULL
) ON [PRIMARY]
GO
Uložená procedura SQL Příklady záznamů
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')
Provést uloženou proceduru pouze se zobrazením
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 1
Výstup je správný, to jsou duplicitní záznamy v tabulce.
Provést uloženou proceduru pouze bez zobrazení
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 0
Uložená procedura fungovala podle očekávání a duplikáty byly úspěšně vyčištěny.
Zvláštní případy pro tuto uloženou proceduru v SQL
Pokud zadané schéma nebo tabulka ve vaší databázi neexistuje, uložená procedura vás upozorní a skript ukončí své provádění.
Pokud necháte název schématu prázdný, skript vás upozorní a ukončí jeho provádění.
Pokud necháte název tabulky prázdný, skript vás na to upozorní a ukončí jeho provádění.
Pokud provedete uloženou proceduru proti tabulce, která nemá žádné duplikáty, a aktivujete bit @displayOnly , dostanete prázdnou sadu výsledků.
Uložená procedura SQL Server:Dokončete kód
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
Závěr
Pokud nevíte, jak odstranit duplicitní záznamy v SQL tabulce, budou pro vás užitečné nástroje, jako je tento. Jakýkoli správce databází může zkontrolovat, zda existují databázové tabulky, které pro ně nemají primární klíče (ani jedinečná omezení), což by mohlo časem nashromáždit hromadu nepotřebných záznamů (potenciálně plýtvat úložištěm). Stačí zapojit a spustit uloženou proceduru a můžete začít.
Můžete jít ještě o něco dále a vytvořit výstražný mechanismus, který vás upozorní, pokud existují duplikáty pro konkrétní tabulku (samozřejmě po implementaci trochu automatizace pomocí tohoto nástroje), což je docela užitečné.
Stejně jako u všeho, co souvisí s úlohami DBA, ujistěte se, že vždy vše otestujete v prostředí izolovaného prostoru, než stisknete spoušť v produkci. A když tak učiníte, ujistěte se, že máte zálohu tabulky, na kterou se zaměřujete.