Bagaimana cara menghapus entri duplikat?


92

Saya harus menambahkan batasan unik ke tabel yang ada. Ini bagus kecuali bahwa tabel sudah memiliki jutaan baris, dan banyak baris yang melanggar batasan unik yang perlu saya tambahkan.

Apa pendekatan tercepat untuk menghilangkan baris yang melanggar? Saya memiliki pernyataan SQL yang menemukan duplikat dan menghapusnya, tetapi butuh waktu lama untuk dijalankan. Apakah ada cara lain untuk mengatasi masalah ini? Mungkin mencadangkan tabel, lalu memulihkan setelah batasan ditambahkan?

Jawaban:


101

Misalnya Anda bisa:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;

2
Dapatkah Anda membuatnya berbeda untuk sekelompok kolom. Mungkin "SELECT DISTINCT (ta, tb, tc), * FROM t"?
gjrwebber


36
lebih mudah untuk mengetik: CREATE TABLE tmp AS SELECT ...;. Maka Anda bahkan tidak perlu mencari tahu apa tata tmpletaknya. :)
Randal Schwartz

9
Jawaban ini sebenarnya tidak terlalu bagus karena beberapa alasan. @Randal bernama satu. Dalam kebanyakan kasus, terutama jika Anda memiliki objek yang bergantung seperti indeks, batasan, tampilan, dll., Pendekatan yang lebih baik adalah menggunakan TABEL TEMPORER aktual , POTONG yang asli dan masukkan kembali data.
Erwin Brandstetter

7
Anda benar tentang indeks. Menjatuhkan & membuat ulang jauh lebih cepat. Tetapi objek lain yang bergantung akan merusak atau mencegah menjatuhkan tabel sama sekali - yang akan diketahui OP setelah membuat salinannya - begitu banyak untuk "pendekatan tercepat". Tetap saja, Anda benar tentang suara negatif tersebut. Itu tidak berdasar, karena ini bukanlah jawaban yang buruk. Tidak begitu bagus. Anda bisa menambahkan beberapa petunjuk tentang indeks atau tergantung objek atau link ke manual seperti yang Anda lakukan di komentar atau penjelasan apa pun . Saya rasa saya frustrasi tentang cara orang memilih. Menghilangkan suara negatif.
Erwin Brandstetter

173

Beberapa dari pendekatan ini tampak sedikit rumit, dan saya biasanya melakukan ini sebagai:

Tabel yang diberikan table, ingin membuatnya unik di (bidang1, bidang2) menjaga baris dengan bidang3 maks:

DELETE FROM table USING table alias 
  WHERE table.field1 = alias.field1 AND table.field2 = alias.field2 AND
    table.max_field < alias.max_field

Misalnya, saya memiliki tabel, user_accountsdan saya ingin menambahkan batasan unik pada email, tetapi saya memiliki beberapa duplikat. Katakan juga bahwa saya ingin menyimpan yang paling baru dibuat (id maks di antara duplikat).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Catatan - USINGbukan SQL standar, ini adalah ekstensi PostgreSQL (tetapi sangat berguna), tetapi pertanyaan asli secara khusus menyebutkan PostgreSQL.

4
Pendekatan kedua itu sangat cepat di postgres! Terima kasih.
Eric Bowman - abstracto -

5
@Tim dapatkah Anda menjelaskan lebih baik apa yang USINGdilakukan di postgresql?
Fopa Léon Constantin

3
Sejauh ini, ini adalah jawaban terbaik. Bahkan jika Anda tidak memiliki kolom serial di tabel Anda untuk digunakan sebagai perbandingan id, ada baiknya untuk sementara menambahkan satu kolom untuk menggunakan pendekatan sederhana ini.
Shane

2
Aku baru saja memeriksa. Jawabannya adalah ya, itu akan. Menggunakan kurang-dari (<) membuat Anda hanya memiliki id maks, sementara lebih besar-dari (>) membuat Anda hanya memiliki id minimum, menghapus sisanya.
André C. Andersen

1
@Shane seseorang dapat menggunakan: WHERE table1.ctid<table2.ctid- tidak perlu menambahkan kolom serial
alexkovelsky

25

Alih-alih membuat tabel baru, Anda juga dapat memasukkan kembali baris unik ke dalam tabel yang sama setelah memotongnya. Lakukan semuanya dalam satu transaksi . Secara opsional, Anda dapat menghapus tabel sementara di akhir transaksi secara otomatis dengan ON COMMIT DROP. Lihat di bawah.

Pendekatan ini hanya berguna jika ada banyak baris yang harus dihapus dari seluruh tabel. Untuk beberapa duplikat, gunakan polos DELETE.

Anda menyebutkan jutaan baris. Untuk membuat operasi cepat, Anda ingin mengalokasikan buffer sementara yang cukup untuk sesi tersebut. Pengaturan harus disesuaikan sebelum buffer sementara apa pun digunakan dalam sesi Anda saat ini. Cari tahu ukuran meja Anda:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Atur temp_bufferssesuai. Kumpulkan dengan murah hati karena representasi dalam memori membutuhkan lebih banyak RAM.

SET temp_buffers = 200MB;    -- example value

BEGIN;

-- CREATE TEMPORARY TABLE t_tmp ON COMMIT DROP AS -- drop temp table at commit
CREATE TEMPORARY TABLE t_tmp AS  -- retain temp table after commit
SELECT DISTINCT * FROM tbl;  -- DISTINCT folds duplicates

TRUNCATE tbl;

INSERT INTO tbl
SELECT * FROM t_tmp;
-- ORDER BY id; -- optionally "cluster" data while being at it.

COMMIT;

Metode ini bisa lebih baik daripada membuat tabel baru jika tergantung objek yang ada. Tampilan, indeks, kunci asing, atau objek lain yang mereferensikan tabel. TRUNCATEmembuat Anda tetap memulai dengan papan tulis yang bersih (file baru di latar belakang) dan jauh lebih cepat dibandingkan DELETE FROM tbldengan tabel besar ( DELETEsebenarnya bisa lebih cepat dengan tabel kecil).

Untuk tabel besar, biasanya lebih cepat untuk menghapus indeks dan kunci asing, mengisi ulang tabel, dan membuat ulang objek ini. Sejauh menyangkut kendala fk, Anda harus yakin bahwa data baru tentu saja valid atau Anda akan mengalami pengecualian saat mencoba membuat fk.

Perhatikan bahwa TRUNCATEmembutuhkan penguncian yang lebih agresif daripada DELETE. Ini mungkin menjadi masalah untuk tabel dengan beban yang berat dan bersamaan.

Jika TRUNCATEbukan merupakan pilihan atau umumnya untuk tabel kecil hingga menengah, ada teknik serupa dengan CTE pemodifikasi data (Postgres 9.1 +):

WITH del AS (DELETE FROM tbl RETURNING *)
INSERT INTO tbl
SELECT DISTINCT * FROM del;
-- ORDER BY id; -- optionally "cluster" data while being at it.

Lebih lambat untuk tabel besar, karena TRUNCATElebih cepat ke sana. Tetapi mungkin lebih cepat (dan lebih sederhana!) Untuk tabel kecil.

Jika Anda tidak memiliki objek bergantung sama sekali, Anda dapat membuat tabel baru dan menghapus yang lama, tetapi Anda hampir tidak mendapatkan apa-apa dari pendekatan universal ini.

Untuk tabel yang sangat besar yang tidak sesuai dengan RAM yang tersedia , membuat tabel baru akan jauh lebih cepat. Anda harus mempertimbangkan ini terhadap kemungkinan masalah / overhead dengan objek yang bergantung.


2
Saya menggunakan pendekatan ini juga. Namun, mungkin pribadi, tetapi tabel temp saya telah dihapus, dan tidak tersedia setelah pemotongan ... Berhati-hatilah untuk melakukan langkah-langkah tersebut jika tabel temp berhasil dibuat dan tersedia.
xlash

@xlash: Anda dapat memeriksa keberadaan untuk memastikan, dan menggunakan nama yang berbeda untuk tabel temp atau menggunakan kembali yang ada .. Saya menambahkan sedikit ke jawaban saya.
Erwin Brandstetter

PERINGATAN: Hati-hati memberi +1 pada @xlash - Saya harus mengimpor ulang data saya karena tabel sementara tidak ada setelahnya TRUNCATE. Seperti yang dikatakan Erwin, pastikan untuk memastikannya ada sebelum memotong meja Anda. Lihat jawaban @ codebyat
Jordan Arseno

1
@JordanArseno: Saya beralih ke versi tanpa ON COMMIT DROP, sehingga orang yang melewatkan bagian di mana saya menulis "dalam satu transaksi" tidak kehilangan data. Dan saya menambahkan BEGIN / COMMIT untuk memperjelas "satu transaksi".
Erwin Brandstetter

1
solusi dengan MENGGUNAKAN memakan waktu lebih dari 3 jam di atas meja dengan 14 juta catatan. Solusi dengan temp_buffers ini membutuhkan waktu 13 menit. Terima kasih.
castt

20

Anda dapat menggunakan oid atau ctid, yang biasanya merupakan kolom "tidak terlihat" di tabel:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);

4
Untuk menghapus di tempat , NOT EXISTSharus jauh lebih cepat : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- atau gunakan kolom atau kumpulan kolom lain untuk menyortir untuk memilih orang yang selamat.
Erwin Brandstetter

@ErwinBrandstetter, apakah kueri yang Anda berikan seharusnya digunakan NOT EXISTS?
Yohanes

1
@ John: Pasti di EXISTSsini. Bacalah seperti ini: "Hapus semua baris di mana ada baris lain dengan nilai yang sama dist_coltetapi lebih besar ctid". Satu-satunya orang yang selamat dari setiap kelompok korban penipuan akan menjadi yang terbesar ctid.
Erwin Brandstetter

Solusi termudah jika Anda hanya memiliki beberapa baris yang digandakan. Dapat digunakan dengan LIMITjika Anda mengetahui jumlah duplikatnya.
Skippy le Grand Gourou

19

Fungsi jendela PostgreSQL berguna untuk masalah 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);

Lihat Menghapus duplikat .


Dan menggunakan "ctid" sebagai ganti "id", ini sebenarnya berfungsi untuk baris duplikat penuh.
bradw2k

Solusi bagus. Saya harus melakukan ini untuk tabel dengan satu miliar catatan. Saya menambahkan WHERE ke bagian dalam SELECT untuk melakukannya dalam potongan.
Jan

7

Dari milis postgresql.org lama :

create table test ( a text, b text );

Nilai-nilai unik

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

Nilai duplikat

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

Satu lagi duplikat ganda

insert into test values ( 'x', 'y');

select oid, a, b from test;

Pilih baris duplikat

select o.oid, o.a, o.b from test o
    where exists ( select 'x'
                   from test i
                   where     i.a = o.a
                         and i.b = o.b
                         and i.oid < o.oid
                 );

Hapus baris duplikat

Catatan: PostgreSQL tidak mendukung alias di tabel yang disebutkan di fromklausul penghapusan.

delete from test
    where exists ( select 'x'
                   from test i
                   where     i.a = test.a
                         and i.b = test.b
                         and i.oid < test.oid
             );

Penjelasan Anda sangat cerdas, tetapi Anda kehilangan satu poin, Dalam buat tabel tentukan oid lalu hanya akses tampilan pesan kesalahan oid else
Kalanidhi

@Kalanidhi Terima kasih atas komentar Anda terkait peningkatan jawaban, saya akan mempertimbangkan hal ini.
Bhavik Ambani

Ini benar-benar datang dari postgresql.org/message-id/…
Martin F

Anda dapat menggunakan kolom sistem 'ctid' jika 'oid' memberi Anda kesalahan.
sul4bh

7

Kueri umum untuk menghapus duplikat:

DELETE FROM table_name
WHERE ctid NOT IN (
  SELECT max(ctid) FROM table_name
  GROUP BY column1, [column 2, ...]
);

Kolom ctidadalah kolom khusus yang tersedia untuk setiap tabel tetapi tidak terlihat kecuali disebutkan secara khusus. Nilai ctidkolom dianggap unik untuk setiap baris dalam tabel.


satu-satunya jawaban universal! Bekerja tanpa self / cartesian JOIN. Perlu ditambahkan bahwa itu penting untuk menentukan GROUP BYklausa dengan benar - ini harus menjadi 'kriteria keunikan' yang dilanggar sekarang atau jika Anda ingin kunci untuk mendeteksi duplikat. Jika ditentukan salah, itu tidak akan berfungsi dengan benar
msciwoj

4

Saya baru saja menggunakan jawaban Erwin Brandstetter berhasil menghapus duplikat dalam tabel gabungan (tabel yang tidak memiliki ID utamanya sendiri), tetapi menemukan bahwa ada satu peringatan penting.

Termasuk ON COMMIT DROPberarti tabel sementara akan dijatuhkan di akhir transaksi. Bagi saya, itu berarti tabel sementara tidak lagi tersedia pada saat saya memasukkannya!

Saya baru saja melakukannya CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;dan semuanya bekerja dengan baik.

Tabel sementara tidak dihilangkan di akhir sesi.


3

Fungsi ini menghapus duplikat tanpa menghapus indeks dan melakukannya ke tabel mana pun.

Pemakaian: select remove_duplicates('mytable');

---
--- remove_duplicates (tablename) menghapus catatan duplikat dari tabel (ubah dari set ke set unik)
---
BUAT ATAU GANTI FUNGSI remove_duplicates (teks) KEMBALI batal AS $$
MENYATAKAN
  tablename ALIAS UNTUK $ 1;
MULAI
  Jalankan 'BUAT TABEL TEMPORER _DISTINCT_' || tablename || 'AS (SELECT DISTINCT * FROM' || tablename || ');';
  Jalankan 'DELETE FROM' || tablename || ';';
  Jalankan 'INSERT INTO' || tablename || '(SELECT * FROM _DISTINCT_' || tablename || ');';
  LAKUKAN 'DROP TABLE _DISTINCT_' || tablename || ';';
  KEMBALI;
AKHIR;
$$ LANGUAGE plpgsql;

3
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);

Itulah yang saya lakukan saat ini, tetapi butuh waktu lama untuk menjalankannya.
gjrwebber

1
Bukankah ini akan gagal jika beberapa baris dalam tabel memiliki nilai yang sama di kolom sesuatu?
shreedhar

3

Jika Anda hanya memiliki satu atau beberapa entri duplikat, dan mereka memang duplikat (yaitu, muncul dua kali), Anda dapat menggunakan kolom "tersembunyi" ctid, seperti yang diusulkan di atas, bersama dengan LIMIT:

DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);

Ini hanya akan menghapus baris pertama yang dipilih.


Saya tahu itu tidak mengatasi masalah OP, yang memiliki banyak duplikat dalam jutaan baris, tetapi itu mungkin bisa membantu.
Skippy le Grand Gourou

Ini harus dijalankan sekali untuk setiap baris duplikat. Jawaban shekwi hanya perlu dijalankan sekali.
bradw2k

3

Pertama, Anda perlu memutuskan "duplikat" mana yang akan Anda simpan. Jika semua kolom sama, OK, Anda dapat menghapus salah satu dari mereka ... Tetapi mungkin Anda hanya ingin menyimpan yang terbaru, atau beberapa kriteria lain?

Cara tercepat bergantung pada jawaban Anda atas pertanyaan di atas, dan juga pada% duplikat di tabel. Jika Anda membuang 50% baris Anda, lebih baik Anda melakukannya CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;, dan jika Anda menghapus 1% baris, menggunakan DELETE lebih baik.

Juga untuk operasi pemeliharaan seperti ini, umumnya baik untuk mengatur work_memsebagian besar RAM Anda: jalankan JELASKAN, periksa nomor N jenis / hash, dan setel work_mem ke RAM / 2 / N. Gunakan banyak RAM; itu bagus untuk kecepatan. Selama Anda hanya memiliki satu koneksi bersamaan ...


1

Saya bekerja dengan PostgreSQL 8.4. Ketika saya menjalankan kode yang diusulkan, saya menemukan bahwa itu tidak benar-benar menghapus duplikat. Dalam menjalankan beberapa tes, saya menemukan bahwa menambahkan "DISTINCT ON (duplikat_kolom_namaku)" dan "ORDER BY duplikat_kolom_namaku" berhasil. Saya bukan ahli SQL, saya menemukan ini di dokumen PostgreSQL 8.4 SELECT ... DISTINCT.

CREATE OR REPLACE FUNCTION remove_duplicates(text, text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
  duplicate_column ALIAS FOR $2;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT ON (' || duplicate_column || ') * FROM ' || tablename || ' ORDER BY ' || duplicate_column || ' ASC);';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;

1

Ini bekerja dengan sangat baik dan sangat cepat:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;

1
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);

Hapus duplikat menurut kolom dan pertahankan baris dengan id terendah. Polanya diambil dari wiki postgres

Dengan menggunakan CTE Anda dapat mencapai versi yang lebih mudah dibaca di atas melalui ini

WITH duplicate_ids as (
    SELECT id, rnum 
    FROM num_of_rows
    WHERE rnum > 1
),
num_of_rows as (
    SELECT id, 
        ROW_NUMBER() over (partition BY column1, 
                                        column2, 
                                        column3 ORDER BY id) AS rnum
        FROM tablename
)
DELETE FROM tablename 
WHERE id IN (SELECT id from duplicate_ids)

1
CREATE TABLE test (col text);
INSERT INTO test VALUES
 ('1'),
 ('2'), ('2'),
 ('3'),
 ('4'), ('4'),
 ('5'),
 ('6'), ('6');
DELETE FROM test
 WHERE ctid in (
   SELECT t.ctid FROM (
     SELECT row_number() over (
               partition BY col
               ORDER BY col
               ) AS rnum,
            ctid FROM test
       ORDER BY col
     ) t
    WHERE t.rnum >1);

Saya mengujinya, dan berhasil; Saya memformatnya agar mudah dibaca. Kelihatannya cukup canggih, tapi perlu penjelasan. Bagaimana seseorang akan mengubah contoh ini untuk kasus penggunaannya sendiri?
Tobias
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.