Çift girişler nasıl silinir?


92

Mevcut bir tabloya benzersiz bir kısıtlama eklemem gerekiyor. Tablonun zaten milyonlarca satıra sahip olması ve satırların çoğunun eklemem gereken benzersiz kısıtlamayı ihlal etmesi dışında bu sorun değil.

Sorun teşkil eden satırları kaldırmak için en hızlı yaklaşım nedir? Yinelenenleri bulup silen bir SQL deyimim var, ancak çalıştırmak sonsuza kadar sürüyor. Bu sorunu çözmenin başka bir yolu var mı? Belki tabloyu yedekledikten sonra kısıtlama eklendikten sonra geri yükleyebilirsiniz?

Yanıtlar:


101

Örneğin şunları yapabilirsiniz:

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

2
Sütun grubu için ayırabilir misin? Belki "SELECT DISTINCT (ta, tb, tc), * FROM t"?
gjrwebber


36
kolay yazmak için: CREATE TABLE tmp AS SELECT ...;. O zaman düzeninin ne olduğunu anlamanıza bile gerek yok tmp. :)
Randal Schwartz

9
Bu cevap aslında birkaç nedenden dolayı pek iyi değil. @Randal biri. Çoğu durumda, var özellikle vb dizinler, kısıtlamaları, görüş gibi nesneler olarak, üstün bir yaklaşım, gerçek kullanmaktır GEÇİCİ TABLE , KESILMESINDEN özgün ve yeniden sokma verileri.
Erwin Brandstetter

7
Dizinler konusunda haklısınız. Düşürmek ve yeniden oluşturmak çok daha hızlıdır. Ancak diğer bağımlı nesneler, - OP'nin kopyalamayı yaptıktan sonra öğreneceği - "en hızlı yaklaşım" için çok fazla masayı kıracak veya tamamen düşürmeyi engelleyecektir . Yine de olumsuz oy konusunda haklısın. Asılsızdır çünkü kötü bir cevap değildir. O kadar da iyi değil. Açıklamada veya herhangi bir açıklamada yaptığınız gibi, indeksler veya bağlı nesneler hakkında bazı ipuçları veya kılavuza bir bağlantı ekleyebilirdiniz . Sanırım insanların nasıl oy kullandığı konusunda hayal kırıklığına uğradım. Olumsuz oy kaldırıldı.
Erwin Brandstetter

173

Bu yaklaşımlardan bazıları biraz karmaşık görünüyor ve bunu genellikle şu şekilde yapıyorum:

Verilen tablo table, satırı max field3 ile tutarak onu (alan1, alan2) üzerinde benzersizleştirmek isteyin:

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

Örneğin, bir masam var user_accountsve e-postaya benzersiz bir kısıtlama eklemek istiyorum, ancak bazı kopyalarım var. En son oluşturulmuş olanı saklamak istediğimi de söyleyin (kopyalar arasında maksimum kimlik).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Not - USINGstandart bir SQL değildir, bir PostgreSQL uzantısıdır (ama çok yararlıdır), ancak asıl soru özellikle PostgreSQL'den bahseder.

4
Bu ikinci yaklaşım postgres'te çok hızlı! Teşekkürler.
Eric Bowman - abstracto -

5
@Tim USINGpostgresql'de ne yaptığını daha iyi açıklayabilir misin?
Fopa Léon Constantin

3
Bu açık ara en iyi cevap. Tablonuzda kimlik karşılaştırması için kullanabileceğiniz bir seri sütun olmasa bile, bu basit yaklaşımı kullanmak için geçici olarak bir tane eklemeye değer.
Shane

2
Az önce kontrol ettim. Cevap evet, olacak. Küçüktür (<) kullanmak sizi yalnızca maks. İd ile bırakır, büyüktür (>) ise yalnızca minimum kimliği bırakır ve geri kalanını siler.
André C. Andersen

1
@Shane şunları kullanabilir: WHERE table1.ctid<table2.ctid- seri sütun eklemeye gerek yok
alexkovelsky

25

Yeni bir tablo oluşturmak yerine, aynı tabloyu kestikten sonra aynı tabloya benzersiz satırlar da ekleyebilirsiniz. Hepsini tek bir işlemde yapın .

Bu yaklaşım, yalnızca tablonun her yerinden silinecek çok sayıda satır olduğunda kullanışlıdır. Sadece birkaç kopya için düz kullanın DELETE.

Milyonlarca satırdan bahsettiniz. İşlemi hızlandırmak için, oturum için yeterli geçici arabellek ayırmak istiyorsunuz . Geçerli oturumunuzda herhangi bir geçici tampon kullanılmadan önce ayarın yapılması gerekir . Masanızın boyutunu bulun:

SELECT pg_size_pretty(pg_relation_size('tbl'));

temp_buffersEn azından biraz daha üstüne ayarlayın .

SET temp_buffers = 200MB;   -- example value

BEGIN;

CREATE TEMP TABLE t_tmp AS  -- retains temp for duration of session
SELECT DISTINCT * FROM tbl  -- DISTINCT folds duplicates
ORDER  BY id;               -- optionally "cluster" data

TRUNCATE tbl;

INSERT INTO tbl
SELECT * FROM t_tmp;        -- retains order (implementation detail)

COMMIT;

Bu yöntem, bağlı nesneler varsa , yeni bir tablo oluşturmaya göre daha üstün olabilir . Tabloya referans veren görünümler, dizinler, yabancı anahtarlar veya diğer nesneler. TRUNCATEyine de temiz bir sayfayla başlamanızı sağlar (arka planda yeni dosya) ve büyük tablolardan çok daha hızlıdır DELETE FROM tbl( DELETEaslında küçük tablolarda daha hızlı olabilir).

Büyük masalar için düzenli olarak daha hızlıdır dizinleri ve yabancı anahtarları (FK) bırakmak, tabloyu yeniden doldurmak ve bu nesneleri yeniden oluşturmak . FK kısıtlamaları söz konusu olduğunda, yeni verilerin geçerli olduğundan emin olmalısınız, yoksa FK'yi oluşturmaya çalışırken istisnalarla karşılaşırsınız.

Bundan TRUNCATEdaha agresif kilitleme gerektirdiğini unutmayın DELETE. Bu, ağır, eşzamanlı yüke sahip tablolar için bir sorun olabilir. Ancak yine de masayı tamamen bırakıp değiştirmekten daha az rahatsız edici.

Bir TRUNCATEseçenek değilse veya genellikle küçük ila orta büyüklükteki tablolar için , veri modifiye edici CTE ile benzer bir teknik vardır (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.

Büyük masalar için daha yavaş çünkü TRUNCATE için daha orada daha hızlı. Ancak küçük masalar için daha hızlı (ve daha basit!) Olabilir.

Bağlı nesneleriniz yoksa, yeni bir masa oluşturabilir ve eskisini silebilirsiniz, ancak bu evrensel yaklaşımdan neredeyse hiçbir şey elde edemezsiniz.

Mevcut RAM'e sığmayan çok büyük tablolar için yeni bir tablo daha hızlı olacaktır. Bunu, bağlı nesnelerle olası sorunlara / ek yüklere karşı tartmanız gerekecek.


2
Ben de bu yaklaşımı kullandım. Ancak, kişisel olabilir, ancak geçici tablom silindi ve kesmeden sonra kullanılamaz ... Geçici tablo başarıyla oluşturulduysa ve kullanılabilirse bu adımları yapmaya dikkat edin.
xlash

@xlash: Emin olmak için var olup olmadığını kontrol edebilir ve geçici tablo için farklı bir isim kullanabilir veya var olanı yeniden kullanabilirsiniz .. Cevabıma biraz ekledim.
Erwin Brandstetter

UYARI: +1 ile @xlash arasında dikkatli olun - Verilerimi yeniden içe aktarmam gerekiyor çünkü daha sonra geçici tablo mevcut değildi TRUNCATE. Erwin'in dediği gibi, tablonuzu kesmeden önce var olduğundan emin olun. @ Codebykat'ın cevabına bakın
Jordan Arseno

1
@JordanArseno: ON COMMIT DROP"Tek işlemde" yazdığım kısmı kaçıran insanlar veri kaybetmesin diye, onsuz bir sürüme geçtim. Ve "bir işlemi" netleştirmek için BEGIN / COMMIT ekledim.
Erwin Brandstetter

1
KULLANMA ile çözüm, 14 milyon kayıtla masada 3 saatten fazla sürdü. Temp_buffers ile bu çözüm 13 dakika sürdü. Teşekkürler.
2015

20

Tabloda normalde "görünmeyen" bir sütun olan oid veya ctid'ı kullanabilirsiniz:

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

4
Yerinde silmek için , NOT EXISTSönemli ölçüde daha hızlı olmalıdır : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- veya hayatta kalanı seçmek için sıralama için başka herhangi bir sütun veya sütun kümesini kullanın.
Erwin Brandstetter

@ErwinBrandstetter, sağladığınız sorgu kullanmanız gerekiyor NOT EXISTSmu?
John

1
@John: Burada olmalı EXISTS. Şöyle okuyun: "İçinde aynı değere sahip dist_colancak daha büyük olan diğer satırların bulunduğu tüm satırları silin ctid". Çifte grup başına hayatta kalan tek kişi, en büyük olanı olacaktır ctid.
Erwin Brandstetter

Yalnızca birkaç yinelenen satırınız varsa en kolay çözüm. LIMITYinelenenlerin sayısını biliyorsanız ile birlikte kullanılabilir .
Skippy le Grand Gourou

19

PostgreSQL pencere işlevi bu sorun için kullanışlıdır.

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

Yinelenenleri silme konusuna bakın .


Ve "id" yerine "ctid" kullanıldığında, bu aslında tamamen yinelenen satırlar için işe yarar.
bradw2k

Harika çözüm. Bunu bir milyar kaydı olan bir tablo için yapmak zorundaydım. Parçalar halinde yapmak için iç SELECT'e bir WHERE ekledim.
Jan

8

Yinelenenleri silmek için genelleştirilmiş sorgu:

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

Sütun ctid, her tablo için mevcut olan özel bir sütundur , ancak özellikle belirtilmedikçe görünmez. ctidSütun değeri, bir tablodaki her satır için benzersiz olarak kabul edilir. Daha fazla bilgi edinmek için PostgreSQL sistem sütunlarına bakın ctid.


1
tek evrensel cevap! Kendi kendine / kartezyen JOIN olmadan çalışır. Maddeyi doğru bir şekilde belirtmenin gerekli olduğunu eklemeye değer GROUP BY- bu, şu anda ihlal edilen 'benzersizlik kriteri' olmalı veya anahtarın kopyaları tespit etmesini istiyorsanız. Yanlış belirtilirse düzgün çalışmaz
msciwoj

7

Gönderen eski postgresql.org posta listesine :

create table test ( a text, b text );

Benzersiz değerler

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

Yinelenen değerler

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

Bir tane daha çift kopya

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

select oid, a, b from test;

Yinelenen satırları seçin

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

Yinelenen satırları silin

Not: PostgreSQL from, silme maddesinde belirtilen tablodaki diğer adları desteklemez .

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

Açıklamanız çok akıllıca, ancak bir noktayı kaçırıyorsunuz, Tablo oluşturda
oid'i

@Kalanidhi Cevabın iyileştirilmesi ile ilgili yorumlarınız için teşekkürler, bu noktayı dikkate alacağım.
Bhavik Ambani


'Oid' size bir hata verirse, 'ctid' sistem sütununu kullanabilirsiniz.
sul4bh

4

Sadece kullanılan Erwin Brandstetter yanıtını bir tablo (tablo kendi birincil kimlikleri eksik) katılmak çiftleri kaldırmak için başarıyla fakat şu unutulmamalıdır olduğunu tespit ettik.

Dahil ON COMMIT DROPetmek, geçici tablonun işlemin sonunda kaldırılacağı anlamına gelir. Benim için bu, onu yerleştirmeye gittiğimde geçici tablonun artık mevcut olmadığı anlamına geliyordu !

Sadece yaptım CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;ve her şey yolunda gitti.

Oturumun sonunda geçici masa düşüyor.


3

Bu işlev, dizinleri kaldırmadan kopyaları kaldırır ve herhangi bir tabloya yapar.

Kullanım: select remove_duplicates('mytable');

---
--- remove_duplicates (tablename), bir tablodan yinelenen kayıtları kaldırır (kümeden benzersiz kümeye dönüştür)
---
FONKSİYON OLUŞTURUN VEYA DEĞİŞTİRİN remove_duplicates (metin) RETURNS void AS $$
BİLDİRMEK
  1 $ İÇİN ALIAS tablename;
BAŞLA
  YÖNET 'GEÇİCİ TABLO OLUŞTUR _DISTINCT_' || tablename || 'AS (SEÇİN DISTINCT * FROM' || tablename || ');';
  UYGULA 'SİLİN' || tablename || ';';
  UYGULA 'INSERT INTO' || tablename || '(SEÇİN * _DISTINCT_' || tablename || ');';
  YÖNET 'DROP TABLE _DISTINCT_' || tablename || ';';
  DÖNÜŞ;
SON;
$$ 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);

Şu anda yaptığım şey bu, ancak koşmak çok uzun sürüyor.
gjrwebber

1
Tablodaki birden fazla satırın sütunda aynı değere sahip olması durumunda bu başarısız olmaz mıydı?
shreedhar

3

Yalnızca bir veya birkaç yinelenen girişiniz varsa ve bunlar gerçekten kopyalanmışsa (yani, iki kez görünürler), ctidyukarıda önerildiği gibi "gizli" sütunu aşağıdakilerle birlikte kullanabilirsiniz LIMIT:

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

Bu, seçilen satırların yalnızca ilkini siler.


Milyonlarca satırda çoğaltılmış OP'nin sorununu ele almadığını biliyorum, ancak yine de yardımcı olabilir.
Skippy le Grand Gourou

Bu, her yinelenen satır için bir kez çalıştırılmalıdır. shekwi'nin cevabının yalnızca bir kez çalıştırılması gerekir.
bradw2k

3

Öncelikle, "kopyalarınızdan" hangilerini saklayacağınıza karar vermelisiniz. Tüm sütunlar eşitse, tamam, bunlardan herhangi birini silebilirsiniz ... Ama belki de yalnızca en sonuncuyu veya başka bir ölçütü saklamak istersiniz?

En hızlı yol, yukarıdaki soruya verdiğiniz yanıta ve ayrıca tablodaki yinelenenlerin yüzdesine bağlıdır. Satırlarınızın% 50'sini atarsanız, yapmakta daha iyi CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;olursunuz ve satırların% 1'ini silerseniz DELETE'i kullanmak daha iyidir.

Ayrıca bunun gibi bakım işlemleri için work_mem, RAM'inizin iyi bir parçasını ayarlamak genellikle iyidir : EXPLAIN'i çalıştırın, tür / karma sayılarının sayısını kontrol edin ve work_mem'i RAM / 2 / N'ye ayarlayın. Çok fazla RAM kullanın; hız için iyidir. Sadece bir eşzamanlı bağlantınız olduğu sürece ...


1

PostgreSQL 8.4 ile çalışıyorum. Önerilen kodu çalıştırdığımda, aslında kopyaları kaldırmadığını fark ettim. Bazı testleri çalıştırırken, "DISTINCT ON (duplicate_column_name)" ve "ORDER BY duplicate_sütun_adı" nın eklenmesinin hile yaptığını gördüm. Ben SQL uzmanı değilim, bunu PostgreSQL 8.4 SELECT ... DISTINCT belgesinde buldum.

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

Bu çok güzel çalışıyor ve çok hızlı:

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

Yinelenenleri sütunlara göre silin ve en düşük kimliğe sahip satırı tutun. Desen postgres wiki'den alınmıştır.

CTE'leri kullanarak, bununla yukarıdakilerin daha okunabilir bir versiyonunu elde edebilirsiniz.

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

Test ettim ve işe yaradı; Okunabilirlik için biçimlendirdim. Oldukça karmaşık görünüyor, ancak bazı açıklamalara ihtiyaç duyabilir. Bu örnek kendi kullanım durumu için nasıl değiştirilir?
Tobias
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.