Jak usunąć zduplikowane rekody w tabeli bazy danych?

Czasami zdarza się że, poprzez nie do końca poprawną walidację danych wejściowych lub z powodu błędnego działania aplikacji stworzą nam się w bazie danych zduplikowane rekordy.

Takie niepożądane zduplikowane dane w 90% będą nam przeszkadzać i w najlepszym razie mogą powodować niepotrzebny zamęt w aplikacji. Skutki mogą takżę spowodować dużo poważniejsze problemy jak chociażby rozsynchronizowanie się danych w powiązanych tabelach w bazie.

Co należy robić w przypadku wykrycia zduplikowanych rekordów?

W pierwszej kolejności trzeba dojść do tego w jaki sposób powstały a następnie trzeba usprawnić aplikację w taki sposób aby powstawanie duplikatów było już niemożliwe.
Następnie trzeba wziąć się za zapisane dane i należy usunąć z nich nadmiarowe rekordy.

Usuwanie zduplikowanych rekordów

Usuwać duplikaty można na conajmniej kilka sposobów. Najczęstszą radą spotykaną w internecie to przeniesienie unikalnych rekordów do nowej tymczasowej tabeli, skasowanie wszystkich danych ze starej tabeli, a następnie zgranie z powrotem rekordy unikalne.

Niestety nie jest to metoda ani szybka, ani skuteczna ani bezpieczna.

Na prościutkim przykładzie rzedstawię poniżej najlepszy sposób na pozbycie się duplikatów

Tworzymy przykładową tabelę:

CREATE TABLE people(
	id SERIAL PRIMARY KEY,
	name VARCHAR(100) NOT NULL,
	surname VARCHAR(100) NOT NULL,
	phone VARCHAR(100) NOT NULL,
	email VARCHAR(100) NOT NULL
);

Tabela zawiera autoinkrementowane pole id, które jest kluczem jej głównym i kilka pól przetrzymujących dane.

Wstawiamy zduplikowane rekordy:

INSERT INTO people(name,surname,phone,email) VALUES('Marcin','Maczka','660111111','marcin@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Marcin','Maczka','660111111','marcin@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Agnieszka','Czopek','550123123','aga@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Jas','Kowalski','555123147','jas@nospam.pl');
INSERT INTO people(name,surname,phone,email) VALUES('Jas','Kowalski','555123147','jas@nospam.pl');

Wyświetlamy zawartość tabeli:

SELECT * FROM people;
 id |   name    | surname  |   phone   |      email       
----+-----------+----------+-----------+------------------
  1 | Marcin    | Maczka   | 660111111 | marcin@nospam.pl
  2 | Marcin    | Maczka   | 660111111 | marcin@nospam.pl
  3 | Agnieszka | Czopek   | 550123123 | aga@nospam.pl
  4 | Jas       | Kowalski | 555123147 | jas@nospam.pl
  5 | Jas       | Kowalski | 555123147 | jas@nospam.pl
(5 rows)

Jak widzimy mamy zduplikowane 2 rekordy, pierwszym krokiem do usunięcia rekordów będzie napisanie zapytania, które je wyświetli a następnie
zmienimy SELECT NA DELETE.

SELECT
	id, name, surname, phone, email
FROM
	people
WHERE
	EXISTS (
		SELECT
			NULL
		FROM
			people AS P
		WHERE
			people.name = P.name
			AND people.surname = P.surname
			AND people.phone = P.phone
			AND people.email = P.email
		GROUP BY
			P.name, P.surname, P.phone, P.email
		HAVING
			people.id < MAX(P.id)
	);

Wynikiem zapytania będzie:

 id |  name  | surname  |   phone   |      email       
----+--------+----------+-----------+------------------
  1 | Marcin | Maczka   | 660111111 | marcin@nospam.pl
  4 | Jas    | Kowalski | 555123147 | jas@nospam.pl
(2 rows)

Wyjaśnienie zapytania:

  • SELECT _FIELDS_ FROM people – deklarujemy pola, które checmy wyświetlić
  • WHERE EXISTS( _CONDITION_ ) – które spełniają warunek _CONDITION_
  • SELECT NULL FROM people AS P – w podzapytaniu nic nie wyświetlamy, wykorzystujemy je jedynie do złączenia
  • WHERE _CONDITION_ – joinujemy po wszystkich polach, które mają tworzyć unikalny klucz
  • GROUP BY _FIELDS_ – grupujemy wg. unikalnych pól
  • HAVING people.id < MAX(P.id) – warunek na id

Modyfikujemy teraz powyższe zapytanie SELECT _FIELDS_ na DELETE

DELETE
FROM
	people
WHERE
	EXISTS (
		SELECT
			NULL
		FROM
			people AS P
		WHERE
			people.name = P.name
			AND people.surname = P.surname
			AND people.phone = P.phone
			AND people.email = P.email
		GROUP BY
			P.name, P.surname, P.phone, P.email
		HAVING
			people.id < MAX(P.id)
	);

I otrzymujemy tabele wyczyszczoną z nadmiarowych rekordów.

Jakie to proste.

**********************edit**********************

Jak słusznie zauważył kolega Wild Child powyższe zapytanie nie działa w mySQL.
Przyznaje się bez bicia, że testowałem jedynie w PostgreSQL gdyż powyższy kod SQL
wyglądał na uniwersalny.

Okazało się jednak, że mySQL nie pozwala na jednoczesne wyświetlanie i modyfikowanie
zawartości tabeli. (jednoczesny DELETE w zapytaniu głównym i SELECT w podzapytaniu)

Posiedziałem trochę i wymyśliłem alternatywne zapytanie:

SELECT 
	D.*
FROM 
	people as U
INNER JOIN 
	people AS D ON (
	   	D.name = U.name AND
   		D.surname = U.surname AND
	   	D.phone = U.phone AND
	   	D.email = U.email
	   )
AND 
	D.id > U.id;

Konstrukcja tego zapytania jest nieco inna:

  • Robimy JOINA wewnątrz tabeli po polach, które mają być unikalne
  • Zostawiamy rekord o najniższym id, a resztę kasujemy (warunek AND D.id > U.id)
  • Skróty: D – duplicate, U – unique
  • Oczywiście aby usunąć rekordy należy zamienić słowo SELECT na DELETE

26 komentarzy do “Jak usunąć zduplikowane rekody w tabeli bazy danych?”

  1. Chyba nie do końca wczytałeś się w treść posta – zaprezentowany SELECT wyświetla zduplikowane rekordy a wiec w przypadku gdy bedzie wiecej niz jeden duplikat (np. będzie 5 takich samych rekordów) w tabeli to zapytanie wyswietli wszystkie duplikaty (czyli 4 takie same wiersze). To jest krok selekconowania duplikatow, następnie gdy w następnym kroku zamienimy SELECT na DELETE to kasujemy wszystkie wczesniej wyswietlone duplikaty.
    Gdy po skasowaniu zrobimy jeszcze raz SELECT w tabeli nie bedzie juz duplikatów.

  2. Hm, no skrypt jest prosty, działa, ale… Przy dużej ilości rekordów potrafi strasznie zamulić serwer co czyni go niespecjalnie funkcjonalnym. Niemniej skrypt sam w sobie całkiem ciekawy, na pewno się przyda ;]

  3. aha i jeszcze jedno, wyświetla duplikaty prawidłowo, ale przy próbie usunięcia (czyli z zamienionym select na delete) wyświetla mi się komunikat – #1093 – You can’t specify target table 'people’ for update in FROM clause

  4. Opisany przyklad wcale nie dotyczy prawdziwych duplikatow! jak widac (people.id < MAX(P.id)
    ) maja one w powyzszym przykladzie rozniace sie IDentyfikatory!

  5. post stary ale dodam dla potomnych.
    W MySql’u mamy dostepny inny mechanizm.
    Mianowicie zakladamy unikatowy klucz na wartosci ktore sa zduplikowane ale w troszke inny sposob:

    alter ignore table tabela_z_duplikatami add unique index `unqidx` (duplicate_col1,duplicate_col2);

    Dzieki zastosowaniu slowka „ignore”, mysql usunie zduplikowane wartosci.
    Potem mozemy spokojnie usunac ten index, albo go zostawic, kto jak woli ;)
    Dodam ze opcja dostepna conajmniej od MySql’a 4.x

  6. Usuwanie lub tworzenie indeksów nie wpływa w żaden sposób na dane w bazie gdyż operujemy na „danych pomocniczych”. Można je tworzyć i usuwać wielokrotnie bez żadnych obaw.

  7. @lolek
    dzięki wielkie. Właśnie wykorzystałem ten myk z „ignore” i działa szybko i idealnie.

  8. Od jutra zaczynam odchudzanie, kto sie odchudza ze mna? Znalazłam w google dobry
    sposób na chudniecie, wygoglujcie sobie – xxally mój sukces
    w odchudzaniu

  9. Działa na mysql przy ponad 40 tys rekordow w czasie mniejszym niz 1 sekunda.
    Ogromne dzięki za to:
    alter ignore table osiagniecia add unique index `unqidx` (gracz_id,rodzaj,rodzaj_id);

    :)

  10. Czesc, ja nie mam klucza głównego id. Jak mogę to rozwiązać?

    mam cztery kolumny:

    nr_sklepu, miasto, grupa, szyld
    1111, Wrocław, A, EM
    1122, Gdynia, B, EM
    1133, Poznań, A, EM
    1111, Wrocław, A, EM
    1122, Gdynia, B, EM
    1133, Poznań, A, EM

    Pozdrawiam
    Adrian.

  11. niby ok, ale mam duplikaty duplikatów (1 i 6 ID nie ma i jest ok) ale po co duplikaty
    ID S_ID R_ID Czas
    2 100348379 1605575498 29.08.2017-23:59:44
    3 100348379 1605575498 29.08.2017-23:59:44
    3 100348379 1605575498 29.08.2017-23:59:44
    4 100348379 1605575498 29.08.2017-23:59:44
    4 100348379 1605575498 29.08.2017-23:59:44
    4 100348379 1605575498 29.08.2017-23:59:44
    5 100348379 1605575498 29.08.2017-23:59:44
    5 100348379 1605575498 29.08.2017-23:59:44
    5 100348379 1605575498 29.08.2017-23:59:44
    5 100348379 1605575498 29.08.2017-23:59:44
    7 100348379 1605575498 29.08.2017-23:59:45
    8 100348379 1605575498 29.08.2017-23:59:45

  12. nawet jak zrobię distinct to mam
    Additional information: Określ tabelę zawierającą rekordy do usunięcia.

    Select działa

    „DELETE DISTINCT(D.ID, D.S_ID, D.R_ID, D.Czas) FROM maildata_tab AS U INNER JOIN maildata_tab AS D ON (D.S_ID = U.S_ID AND D.R_ID = U.R_ID AND D.Czas = U.Czas) AND (D.ID > U.ID)”

  13. A co w sytuacji, gdy mam dwa rekordy identyczne, gdzie id (który jest/powinien być PK) też się pokrywa?

  14. You can use the following script to remove duplicate rows from a Microsoft SQL Server table:

    SELECT DISTINCT *
    INTO duplicate_table
    FROM original_table
    GROUP BY key_value
    HAVING COUNT(key_value) > 1

    DELETE original_table
    WHERE key_value
    IN (SELECT key_value
    FROM duplicate_table)

    INSERT original_table
    SELECT *
    FROM duplicate_table

    DROP TABLE duplicate_table

    When this script is executed, it follows these steps:

    It moves one instance of any duplicate row in the original table to a duplicate table.
    It deletes all rows from the original table that also reside in the duplicate table.
    It moves the rows in the duplicate table back into the original table.
    It drops the duplicate table

    wzięte z linku poniżej

    https://support.microsoft.com/pl-pl/help/70956/how-to-remove-duplicate-rows-from-a-sql-server-table-by-using-a-script

  15. Jestem naprawdę pod wrażeniem Twoimi umiejętnościami i jak elegancko piszesz.
    Format na Twoim blogu jest na dobrym poziomie. Czy to jest płatny motyw, czy też zmodyfikowany
    pod siebie? W każdym razie utrzymuj nadal wysoką jakość pisania, to
    rzadkie zobaczyć tak świetny blog w dzisiejszych
    czasach. :)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *