Czasami podczas naszej pracy jako administratorzy baz danych natrafiamy na co najmniej jedną tabelę załadowaną zduplikowanymi rekordami. Nawet jeśli tabela ma klucz podstawowy (w większości przypadków automatycznie przyrostowy), pozostałe pola mogą mieć zduplikowane wartości.
Jednak SQL Server pozwala na wiele sposobów pozbycia się tych zduplikowanych rekordów (np. za pomocą CTE, funkcji SQL Rank, podzapytań z Group By itp.).
Pamiętam, że podczas wywiadu zapytano mnie, jak usunąć zduplikowane rekordy w tabeli, pozostawiając tylko 1 z każdego. Wtedy nie byłam w stanie odpowiedzieć, ale byłam bardzo ciekawa. Po krótkich poszukiwaniach znalazłem wiele opcji rozwiązania tego problemu.
Teraz, po latach, przedstawię Wam procedurę składowaną, która ma odpowiedzieć na pytanie „jak usunąć zduplikowane rekordy w tabeli SQL?”. Każdy administrator DBA może go po prostu użyć do sprzątania, nie martwiąc się zbytnio.
Utwórz procedurę składowaną:uwagi wstępne
Konto, którego używasz, musi mieć wystarczające uprawnienia, aby utworzyć procedurę składowaną w zamierzonej bazie danych.
Konto wykonujące tę procedurę składowaną musi mieć wystarczające uprawnienia, aby wykonać operacje SELECT i DELETE na docelowej tabeli bazy danych.
Ta procedura składowana jest przeznaczona dla tabel bazy danych, które nie mają zdefiniowanego klucza podstawowego (ani ograniczenia UNIQUE). Jeśli jednak tabela ma klucz podstawowy, procedura składowana nie uwzględni tych pól. Przeprowadzi wyszukiwanie i usuwanie na podstawie pozostałych pól (więc w tym przypadku używaj go bardzo ostrożnie).
Jak korzystać z zapisanej procedury w SQL
Skopiuj i wklej kod SP T-SQL dostępny w tym artykule. SP oczekuje 3 parametrów:
@schemaName – nazwa schematu tabeli bazy danych, jeśli ma zastosowanie. Jeśli nie – użyj dbo .
@tableName – nazwa tabeli bazy danych, w której przechowywane są zduplikowane wartości.
@displayOnly – jeśli ustawione na 1 , faktyczne zduplikowane rekordy nie zostaną usunięte , ale wyświetlane tylko zamiast (jeśli istnieje). Domyślnie ta wartość jest ustawiona na 0 co oznacza, że rzeczywiste usunięcie nastąpi jeśli istnieją duplikaty.
Procedura składowana serwera SQL Testy wykonania
Aby zademonstrować procedurę składowaną, stworzyłem dwie różne tabele – jedną bez klucza podstawowego, a drugą z kluczem podstawowym. Do tych tabel wstawiłem kilka fikcyjnych rekordów. Sprawdźmy, jakie wyniki otrzymam przed/po wykonaniu procedury zapisanej.
Tabela SQL z kluczem podstawowym
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
Procedura składowana SQL Rekordy przykładowe
INSERT INTO test VALUES('A','A',1),('A','B',1),('A','C',1),('B','A',2),('B','B',3),('B','C',4)
Wykonaj zapisaną procedurę tylko z wyświetlaniem
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 1
Ponieważ column1 i column2 tworzą klucz podstawowy, duplikaty są oceniane względem kolumn innych niż klucz podstawowy, w tym przypadku kolumny 3. Wynik jest prawidłowy.
Wykonaj procedurę przechowywaną bez wyświetlania tylko
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'test',@displayOnly = 0
Zduplikowane rekordy zniknęły.
Musisz jednak uważać z tym podejściem, ponieważ pierwsze wystąpienie rekordu jest tym, które ucina. Tak więc, jeśli z jakiegokolwiek powodu potrzebujesz bardzo konkretnego rekordu do usunięcia, musisz osobno zająć się swoją konkretną sprawą.
SQL Tabela bez klucza podstawowego
CREATE TABLE [dbo].[duplicates](
[column1] [varchar](16) NOT NULL,
[column2] [varchar](16) NOT NULL,
[column3] [varchar](16) NOT NULL
) ON [PRIMARY]
GO
Procedura składowana SQL Przykładowe rekordy
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')
Wykonaj zapisaną procedurę tylko z wyświetlaniem
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 1
Wynik jest prawidłowy, to są zduplikowane rekordy w tabeli.
Wykonaj procedurę przechowywaną bez wyświetlania tylko
EXEC DBA_DeleteDuplicates @schemaName = 'dbo',@tableName = 'duplicates',@displayOnly = 0
Procedura przechowywana działała zgodnie z oczekiwaniami, a duplikaty zostały pomyślnie wyczyszczone.
Przypadki specjalne dla tej procedury składowanej w SQL
Jeśli określony schemat lub tabela nie istnieje w Twojej bazie danych, procedura składowana powiadomi Cię o tym, a skrypt zakończy wykonywanie.
Jeśli pozostawisz nazwę schematu pustą, skrypt powiadomi Cię o tym i zakończy jego wykonywanie.
Jeśli pozostawisz nazwę tabeli pustą, skrypt powiadomi Cię o tym i zakończy jej wykonywanie.
Jeśli wykonasz procedurę składowaną na tabeli, która nie ma żadnych duplikatów, i aktywujesz bit @displayOnly , otrzymasz pusty zestaw wyników.
Procedura przechowywana w programie SQL Server:kompletny kod
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
Wniosek
Jeśli nie wiesz, jak usunąć zduplikowane rekordy w tabeli SQL, to takie narzędzia będą dla Ciebie pomocne. Każdy administrator baz danych może sprawdzić, czy istnieją tabele bazy danych, które nie mają dla nich kluczy podstawowych (ani ograniczeń unikatowości), co może z czasem gromadzić stos niepotrzebnych rekordów (potencjalnie marnując pamięć). Wystarczy podłączyć i uruchomić procedurę przechowywaną i gotowe.
Możesz pójść trochę dalej i zbudować mechanizm ostrzegania, który będzie powiadamiał Cię, jeśli istnieją duplikaty dla określonej tabeli (oczywiście po wdrożeniu odrobiny automatyzacji za pomocą tego narzędzia), co jest bardzo przydatne.
Podobnie jak w przypadku wszystkiego, co dotyczy zadań DBA, upewnij się, że zawsze testujesz wszystko w środowisku piaskownicy przed naciśnięciem spustu w środowisku produkcyjnym. A kiedy to zrobisz, upewnij się, że masz kopię zapasową stołu, na którym się koncentrujesz.