Hapus Data Duplikat di PostgreSQL

113

Saya memiliki tabel dalam database PostgreSQL 8.3.8, yang tidak memiliki kunci / batasan di atasnya, dan memiliki beberapa baris dengan nilai yang persis sama.

Saya ingin menghapus semua duplikat dan hanya menyimpan 1 salinan untuk setiap baris.

Ada satu kolom khusus (bernama "kunci") yang dapat digunakan untuk mengidentifikasi duplikat (yaitu, hanya boleh ada satu entri untuk setiap "kunci" yang berbeda).

Bagaimana saya bisa melakukan ini? (Idealnya dengan satu perintah SQL) Kecepatan tidak menjadi masalah dalam kasus ini (hanya ada beberapa baris).

André Morujão
sumber

Jawaban:

80
DELETE FROM dupes a
WHERE a.ctid <> (SELECT min(b.ctid)
                 FROM   dupes b
                 WHERE  a.key = b.key);
seekor kuda tanpa nama
sumber
20
Jangan gunakan, ini terlalu lambat!
Paweł Malisak
5
Meskipun solusi ini pasti berhasil, solusi @rapimo di bawah ini dijalankan jauh lebih cepat. Saya percaya ini ada hubungannya dengan pernyataan pilih bagian dalam di sini yang dieksekusi N kali (untuk semua baris N dalam tabel dupes) daripada pengelompokan yang terjadi di solusi lain.
David
Untuk tabel yang sangat besar (beberapa juta catatan), yang satu ini sebenarnya cocok dengan memori, tidak seperti solusi @ rapimo. Jadi dalam kasus tersebut ini adalah yang lebih cepat (tidak ada pertukaran).
Giel
1
Menambahkan penjelasan: ini berfungsi karena ctid adalah kolom postgres khusus yang menunjukkan lokasi fisik baris. Anda dapat menggunakan ini sebagai id unik meskipun tabel Anda tidak memiliki id unik. postgresql.org/docs/8.2/ddl-system-columns.html
Eric Burel
194

Solusi yang lebih cepat adalah

DELETE FROM dups a USING (
      SELECT MIN(ctid) as ctid, key
        FROM dups 
        GROUP BY key HAVING COUNT(*) > 1
      ) b
      WHERE a.key = b.key 
      AND a.ctid <> b.ctid
rapimo
sumber
20
Mengapa ini lebih cepat daripada solusi a_horse_with_no_name?
Roberto
3
Ini lebih cepat karena ini hanya menjalankan 2 kueri. Pertama untuk memilih semua duplikat, lalu satu untuk menghapus semua item dari tabel. Kueri oleh @a_horse_with_no_name melakukan kueri untuk melihat apakah cocok dengan yang lain untuk setiap item dalam tabel.
Aeolun
5
apa ctid?
techkuz
6
dari docs: ctid. Lokasi fisik versi baris dalam tabelnya. Perhatikan bahwa meskipun ctid dapat digunakan untuk menemukan versi baris dengan sangat cepat, ctid baris akan berubah setiap kali diperbarui atau dipindahkan oleh VACUUM FULL. Oleh karena itu ctid tidak berguna sebagai pengenal baris jangka panjang.
Saim
1
Sepertinya ini tidak berfungsi bila memiliki lebih dari 2 baris duplikat, karena hanya menghapus satu duplikat pada satu waktu.
Frankie Drake
73

Ini cepat dan ringkas:

DELETE FROM dupes T1
    USING   dupes T2
WHERE   T1.ctid < T2.ctid  -- delete the older versions
    AND T1.key  = T2.key;  -- add more columns if needed

Lihat juga jawaban saya di Cara menghapus baris duplikat tanpa pengenal unik yang menyertakan lebih banyak informasi.

isapir
sumber
apa kepanjangan dari ct? menghitung?
techkuz
4
@trthhrtz ctidmenunjuk ke lokasi fisik rekaman dalam tabel. Bertentangan dengan apa yang saya tulis pada saat di komentar, menggunakan kurang dari operator tidak selalu mengarah ke versi yang lebih lama karena ct dapat membungkus dan nilai dengan ctid yang lebih rendah mungkin sebenarnya lebih baru.
isapir
1
FYI saja, saya mencoba solusi ini, dan membatalkannya setelah menunggu 15 menit. Mencoba solusi rapimo dan selesai dalam waktu sekitar 10 detik (dihapus ~ 700.000 baris).
Patrick
@Patrick tidak dapat membayangkan jika db Anda tidak memiliki pengenal unik karena jawaban rapimo tidak berfungsi dalam kasus itu.
simpan
@isapir saya hanya penasaran, jawaban di atas, mereka menyimpan catatan lama sesuai pilihannya min(ctid)? sedangkan milikmu menyimpan yang lebih baru? Terima kasih!
simpan
17

Saya mencoba ini:

DELETE FROM tablename
WHERE id IN (SELECT id
              FROM (SELECT id,
                             ROW_NUMBER() OVER (partition BY column1, column2, column3 ORDER BY id) AS rnum
                     FROM tablename) t
              WHERE t.rnum > 1);

disediakan oleh Postgres wiki:

https://wiki.postgresql.org/wiki/Deleting_duplicates

Radu Gabriel
sumber
Adakah gagasan tentang kinerja dibandingkan dengan jawaban @ rapimo dan yang diterima (@a_horse_with_no_name)?
tuxayo
3
Yang ini tidak akan berfungsi jika, seperti pertanyaan menyatakan, semua kolom identik, iddisertakan.
ibizaman
Kueri ini akan menghapus salinan asli dan duplikatnya. pertanyaannya adalah tentang mempertahankan setidaknya satu baris.
pyBomb
@pyBomb salah, itu akan menyimpan yang pertama di idmana kolom1 ... 3 adalah duplikat
Jeff
Pada postgresql 12, sejauh ini solusi tercepat (terhadap 300 juta baris). Saya baru saja menguji semua yang diusulkan dalam pertanyaan ini, termasuk jawaban yang diterima, dan solusi "resmi" ini sebenarnya yang tercepat dan memenuhi semua persyaratan dari OP (dan milik saya)
Jeff
7

Saya harus membuat versi saya sendiri. Versi yang ditulis oleh @a_horse_with_no_name terlalu lambat di tabel saya (21 juta baris). Dan @rapimo sama sekali tidak menghapus dups.

Inilah yang saya gunakan di PostgreSQL 9.5

DELETE FROM your_table
WHERE ctid IN (
  SELECT unnest(array_remove(all_ctids, actid))
  FROM (
         SELECT
           min(b.ctid)     AS actid,
           array_agg(ctid) AS all_ctids
         FROM your_table b
         GROUP BY key1, key2, key3, key4
         HAVING count(*) > 1) c);
ahli
sumber
6

Saya akan menggunakan tabel sementara:

create table tab_temp as
select distinct f1, f2, f3, fn
  from tab;

Kemudian, hapus tabdan ganti nama tab_tempmenjadi tab.

Pablo Santa Cruz
sumber
8
Pendekatan ini tidak memperhitungkan pemicu, indeks, dan statistik. Tentu Anda bisa menambahkannya, tetapi itu juga menambah lebih banyak pekerjaan.
Yordania
Tidak semua orang membutuhkan itu. Pendekatan ini sangat cepat dan bekerja jauh lebih baik daripada yang lain pada 200k email (varchar 250) tanpa indeks.
Sergey Telshevsky
Kode lengkap:DROP TABLE IF EXISTS tmp; CREATE TABLE tmp as ( SELECT * from (SELECT DISTINCT * FROM your_table) as t ); DELETE from your_table; INSERT INTO your_table SELECT * from tmp; DROP TABLE tmp;
Eric Burel
1

Pendekatan lain (hanya berfungsi jika Anda memiliki bidang unik seperti iddi tabel Anda) untuk menemukan semua id unik menurut kolom dan menghapus id lain yang tidak ada dalam daftar unik

DELETE
FROM users
WHERE users.id NOT IN (SELECT DISTINCT ON (username, email) id FROM users);
Zaytsev Dmitry
sumber
Masalahnya, dalam pertanyaan saya, tabel tersebut tidak memiliki id unik; "duplikat" adalah beberapa baris dengan nilai yang sama persis di semua kolom.
André Morujão
Benar, saya menambahkan beberapa catatan
Zaytsev Dmitry
1

Bagaimana tentang:

DENGAN
  u SEBAGAI (PILIH DISTINCT * FROM your_table),
  x SEBAGAI (HAPUS DARI tabel_Anda)
MASUKKAN KE tabel_anda SELECT * FROM u;

Saya khawatir tentang urutan eksekusi, apakah DELETE akan terjadi sebelum SELECT DISTINCT, tetapi berfungsi dengan baik untuk saya. Dan memiliki bonus tambahan karena tidak memerlukan pengetahuan apa pun tentang struktur tabel.

Barrie Walker
sumber
Satu-satunya kelemahan adalah, jika Anda memiliki tipe data yang tidak mendukung kesetaraan (misalnya json), ini tidak akan berfungsi.
a_horse_with_no_name
0

Ini bekerja dengan baik untuk saya. Saya memiliki tabel, istilah, yang berisi nilai duplikat. Menjalankan kueri untuk mengisi tabel temp dengan semua baris duplikat. Lalu saya menjalankan pernyataan delete dengan id tersebut di tabel temp. nilai adalah kolom yang berisi duplikat.

        CREATE TEMP TABLE dupids AS
        select id from (
                    select value, id, row_number() 
over (partition by value order by value) 
    as rownum from terms
                  ) tmp
                  where rownum >= 2;

delete from [table] where id in (select id from dupids)
Beanwah
sumber
0

Berikut solusinya dengan menggunakan PARTITION BY:

DELETE FROM dups
USING (
  SELECT
    ctid,
    (ctid != min(ctid) OVER (PARTITION BY key_column1, key_column2 [...])) AS is_duplicate
  FROM dups 
) dups_find_duplicates
WHERE dups.ctid == dups_find_duplicates.ctid
AND dups_find_duplicates.is_duplicate
LeoRochael
sumber