Làm thế nào để xóa các mục trùng lặp?


92

Tôi phải thêm một ràng buộc duy nhất vào một bảng hiện có. Điều này là tốt, ngoại trừ việc bảng đã có hàng triệu hàng và nhiều hàng vi phạm giới hạn duy nhất mà tôi cần thêm.

Cách tiếp cận nhanh nhất để loại bỏ các hàng vi phạm là gì? Tôi có một câu lệnh SQL tìm các bản sao và xóa chúng, nhưng mất mãi mãi để chạy. Có cách nào khác để giải quyết vấn đề này không? Có thể sao lưu bảng, sau đó khôi phục sau khi ràng buộc được thêm vào?

Câu trả lời:


101

Ví dụ, bạn có thể:

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

2
Bạn có thể làm cho nó khác biệt cho nhóm cột. Có thể "CHỌN DISTINCT (ta, tb, tc), * TỪ t"?
gjrwebber


36
dễ dàng hơn để gõ: CREATE TABLE tmp AS SELECT ...;. Sau đó, bạn thậm chí không cần phải tìm ra bố cục của tmplà gì. :)
Randal Schwartz

9
Câu trả lời này thực sự không tốt lắm vì một số lý do. @Randal có tên một. Trong hầu hết các trường hợp, đặc biệt nếu bạn có các đối tượng phụ thuộc như chỉ mục, ràng buộc, chế độ xem, v.v., cách tiếp cận ưu việt là sử dụng BẢNG TẠM THỜI thực tế , TRUNCATE bản gốc và chèn lại dữ liệu.
Erwin Brandstetter

7
Bạn nói đúng về các chỉ mục. Giảm và tạo lại nhanh hơn nhiều. Nhưng các đối tượng phụ thuộc khác sẽ phá vỡ hoặc ngăn chặn hoàn toàn việc rơi bảng - điều mà OP sẽ tìm ra sau khi tạo bản sao - rất nhiều cho "cách tiếp cận nhanh nhất". Tuy nhiên, bạn nói đúng về phiếu giảm giá. Nó là vô căn cứ, bởi vì nó không phải là một câu trả lời tồi. Nó chỉ là không tốt. Bạn có thể đã thêm một số con trỏ về các chỉ mục hoặc các đối tượng phụ thuộc hoặc một liên kết tới sổ tay giống như bạn đã làm trong nhận xét hoặc bất kỳ loại giải thích nào. Tôi đoán tôi đã thất vọng về cách mọi người bỏ phiếu. Đã xóa phiếu phản đối.
Erwin Brandstetter

173

Một số cách tiếp cận này có vẻ hơi phức tạp và tôi thường làm như sau:

Bảng đã cho table, muốn duy nhất nó trên (field1, field2) giữ hàng có trường max3:

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

Ví dụ: tôi có một bảng user_accountsvà tôi muốn thêm một ràng buộc duy nhất vào email, nhưng tôi có một số bản sao. Cũng nói rằng tôi muốn giữ một cái được tạo gần đây nhất (id tối đa trong số các bản sao).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Lưu ý - USINGkhông phải là SQL tiêu chuẩn, nó là một phần mở rộng PostgreSQL (nhưng rất hữu ích), nhưng câu hỏi ban đầu đề cập cụ thể đến PostgreSQL.

4
Cách tiếp cận thứ hai đó rất nhanh trên postgres! Cảm ơn.
Eric Bowman - abstracto -

5
@Tim bạn có thể giải thích rõ hơn những gì USINGlàm trong postgresql không?
Fopa Léon Constantin

3
Đây là câu trả lời tốt nhất cho đến nay. Ngay cả khi bạn không có cột nối tiếp trong bảng của mình để sử dụng cho việc so sánh id, bạn nên tạm thời thêm một cột để sử dụng phương pháp đơn giản này.
Shane

2
Tôi chỉ cần kiểm tra. Câu trả lời là có, nó sẽ. Việc sử dụng less-than (<) khiến bạn chỉ có id tối đa, trong khi lớn hơn (>) chỉ để lại id tối thiểu cho bạn, xóa phần còn lại.
André C. Andersen

1
@Shane người ta có thể sử dụng: WHERE table1.ctid<table2.ctid- không cần thêm cột nối tiếp
alexkovelsky

25

Thay vì tạo một bảng mới, bạn cũng có thể chèn lại các hàng duy nhất vào cùng một bảng sau khi cắt bớt nó. Làm tất cả trong một lần giao dịch . Theo tùy chọn, bạn có thể tự động bỏ bảng tạm thời vào cuối giao dịch vớiON COMMIT DROP . Xem bên dưới.

Cách tiếp cận này chỉ hữu ích khi có nhiều hàng cần xóa trên toàn bộ bảng. Đối với một vài bản sao, hãy sử dụngDELETE .

Bạn đã đề cập đến hàng triệu hàng. Để làm cho hoạt động nhanh chóng, bạn muốn phân bổ đủ bộ đệm tạm thời cho phiên. Cài đặt phải được điều chỉnh trước khi bất kỳ bộ đệm tạm thời nào được sử dụng trong phiên hiện tại của bạn. Tìm hiểu kích thước bàn của bạn:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Đặt cho temp_buffersphù hợp. Làm tròn một cách hào phóng vì biểu diễn trong bộ nhớ cần nhiều RAM hơn một chút.

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;

Phương pháp này có thể vượt trội hơn so với việc tạo một bảng mới nếu tồn tại các đối tượng phụ thuộc. Chế độ xem, chỉ mục, khóa ngoại hoặc các đối tượng khác tham chiếu đến bảng. TRUNCATElàm cho bạn bắt đầu với một slate sạch anyway (file mới trong nền) và nhiều nhanh hơn DELETE FROM tblvới các bảng lớn (DELETE trên thực tế có thể nhanh hơn với những chiếc bàn nhỏ).

Đối với các bảng lớn, nó thường nhanh hơn việc giảm chỉ mục và khóa ngoại, điền vào bảng và tạo lại các đối tượng . Liên quan đến các ràng buộc fk, bạn phải chắc chắn rằng dữ liệu mới là hợp lệ, nếu không bạn sẽ gặp phải trường hợp ngoại lệ khi cố gắng tạo fk.

Lưu ý rằng TRUNCATEyêu cầu khóa tích cực hơnDELETE . Đây có thể là vấn đề đối với các bảng có tải nặng, đồng thời.

Nếu TRUNCATEkhông phải là một tùy chọn hoặc nói chung đối với các bảng vừa và nhỏ thì có một kỹ thuật tương tự với CTE sửa đổi dữ liệu (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.

Chậm hơn đối với các bảng lớn, vì TRUNCATEở đó nhanh hơn. Nhưng có thể nhanh hơn (và đơn giản hơn!) Cho các bảng nhỏ.

Nếu bạn không có đối tượng phụ thuộc nào cả, bạn có thể tạo một bảng mới và xóa bảng cũ, nhưng bạn hầu như không thu được gì qua cách tiếp cận phổ quát này.

Đối với các bảng rất lớn không vừa với RAM khả dụng , việc tạo một bảng mới sẽ nhanh hơn đáng kể. Bạn sẽ phải cân nhắc điều này trước những rắc rối / chi phí có thể xảy ra với các đối tượng tùy thuộc.


2
Tôi cũng đã sử dụng cách tiếp cận này. Tuy nhiên, có thể là do cá nhân, nhưng bảng tạm thời của tôi đã bị xóa và không có sẵn sau khi cắt ngắn ... Hãy cẩn thận thực hiện các bước đó nếu bảng tạm thời được tạo thành công và có sẵn.
xlash

@xlash: Bạn có thể kiểm tra sự tồn tại để đảm bảo và sử dụng tên khác cho bảng tạm thời hoặc sử dụng lại tên đang tồn tại .. Tôi đã thêm một chút vào câu trả lời của mình.
Erwin Brandstetter

CẢNH BÁO: Hãy cẩn thận +1 tới @xlash - Tôi phải nhập lại dữ liệu của mình vì bảng tạm thời không tồn tại sau đó TRUNCATE. Như Erwin đã nói, hãy đảm bảo rằng nó tồn tại trước khi cắt bớt bảng của bạn. Xem câu trả lời của @ codebykat
Jordan Arseno

1
@JordanArseno: Tôi đã chuyển sang phiên bản không có ON COMMIT DROP, để những người bỏ lỡ phần mà tôi đã viết "trong một giao dịch" không bị mất dữ liệu. Và tôi đã thêm BEGIN / COMMIT để làm rõ "một giao dịch".
Erwin Brandstetter

1
giải pháp với USING mất hơn 3 giờ trên bảng với 14 triệu bản ghi. Giải pháp này với temp_buffers mất 13 phút. Cảm ơn.
castt

20

Bạn có thể sử dụng oid hoặc ctid, thường là các cột "không hiển thị" trong bảng:

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

4
Để xóa tại chỗ , NOT EXISTSsẽ nhanh hơn đáng kể : DELETE FROM tbl t WHERE EXISTS (SELECT 1 FROM tbl t1 WHERE t1.dist_col = t.dist_col AND t1.ctid > t.ctid)- hoặc sử dụng bất kỳ cột nào khác hoặc tập hợp các cột để phân loại để chọn một người sống sót.
Erwin Brandstetter

@ErwinBrandstetter, truy vấn bạn cung cấp có được sử dụng NOT EXISTSkhông?
John

1
@John: Nó phải EXISTSở đây. Đọc nó như thế này: "Xóa tất cả các hàng mà bất kỳ hàng nào khác tồn tại có cùng giá trị trong dist_colnhưng lớn hơn ctid". Người sống sót duy nhất trong mỗi nhóm lừa đảo sẽ là người có nhiều nhất ctid.
Erwin Brandstetter

Giải pháp dễ dàng nhất nếu bạn chỉ có một vài hàng trùng lặp. Có thể được sử dụng với LIMITnếu bạn biết số lượng bản sao.
Skippy le Grand Gourou

19

Chức năng cửa sổ PostgreSQL rất hữu ích cho vấn đề này.

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

Xem Xóa bản sao .


Và sử dụng "ctid" thay vì "id", điều này thực sự hoạt động đối với các hàng hoàn toàn trùng lặp.
bradw2k

Giải pháp tuyệt vời. Tôi đã phải làm điều này cho một bảng có hàng tỷ bản ghi. Tôi đã thêm WHERE vào SELECT bên trong để thực hiện nó theo từng phần.
Ngày

7

Từ danh sách gửi thư cũ của postgresql.org :

create table test ( a text, b text );

Giá trị duy nhất

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

Giá trị trùng lặp

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

Thêm một bản sao kép nữa

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

select oid, a, b from test;

Chọn các hàng trùng lặp

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

Xóa các hàng trùng lặp

Lưu ý: Các dos PostgreSQL không hỗ trợ các bí danh trên bảng được đề cập trong fromđiều khoản xóa.

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

Lời giải thích của bạn là rất thông minh, nhưng bạn đang thiếu một điểm, Trong tạo bảng ghi rõ oid sau đó chỉ truy cập vào màn hình hiển thị thông báo lỗi khác oid
Kalanidhi

@Kalanidhi Cảm ơn ý kiến ​​của bạn về việc cải thiện câu trả lời, tôi sẽ xem xét điểm này.
Bhavik Ambani

Điều này thực sự đến từ postgresql.org/message-id/…
Martin F

Bạn có thể sử dụng cột hệ thống 'ctid' nếu 'oid' cho bạn lỗi.
sul4bh

7

Truy vấn tổng quát để xóa các bản sao:

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

Cột ctidnày là một cột đặc biệt có sẵn cho mọi bảng nhưng không hiển thị trừ khi được đề cập cụ thể. Các ctidgiá trị cột được coi là duy nhất cho mỗi hàng trong một bảng.


câu trả lời phổ quát duy nhất! Hoạt động mà không có bản thân / cartesian THAM GIA. Đáng nói thêm rằng điều cần thiết là phải chỉ định chính xác GROUP BYmệnh đề - đây phải là 'tiêu chí duy nhất' bị vi phạm ngay bây giờ hoặc nếu bạn muốn khóa để phát hiện các bản sao. Nếu được chỉ định sai nó sẽ không hoạt động chính xác
Mściwoj

4

Tôi vừa sử dụng thành công câu trả lời của Erwin Brandstetter để xóa các bản sao trong một bảng tham gia (một bảng thiếu ID chính của riêng nó), nhưng nhận thấy rằng có một điều quan trọng cần lưu ý.

Bao gồm ON COMMIT DROPcó nghĩa là bảng tạm thời sẽ bị xóa khi kết thúc giao dịch. Đối với tôi, điều đó có nghĩa là bảng tạm thời không còn nữa vào thời điểm tôi chèn nó vào!

Tôi chỉ làm CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;và mọi thứ hoạt động tốt.

Bảng tạm thời không bị xóa vào cuối phiên.


3

Hàm này loại bỏ các bản sao mà không xóa chỉ mục và thực hiện nó với bất kỳ bảng nào.

Sử dụng: select remove_duplicates('mytable');

---
--- remove_duplicates (tên bảng) xóa các bản ghi trùng lặp khỏi bảng (chuyển đổi từ tập hợp thành tập hợp duy nhất)
---
TẠO HOẶC THAY THẾ CHỨC NĂNG remove_duplicates (text) RETURNS void AS $$
KHAI BÁO
  tên bảng ALIAS FOR $ 1;
BẮT ĐẦU
  THỰC HIỆN 'TẠO BẢNG TẠM THỜI _DISTINCT_' || tên bảng || 'AS (CHỌN DISTINCT * TỪ' || tên bảng || ');';
  THỰC HIỆN 'XÓA TỪ' || tên bảng || ';';
  THỰC HIỆN 'CHÈN VÀO' || tên bảng || '(CHỌN * TỪ _DISTINCT_' || tên bảng || ');';
  THỰC HIỆN 'DROP TABLE _DISTINCT_' || tên bảng || ';';
  TRỞ VỀ;
KẾT THÚC;
$$ 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);

Đó là những gì tôi hiện đang làm, nhưng phải mất một thời gian rất dài để thực hiện.
gjrwebber

1
Điều này sẽ không thất bại nếu nhiều hàng trong bảng có cùng giá trị trong cột nào đó?
shreedhar

3

Nếu bạn chỉ có một hoặc một vài mục nhập trùng lặp và chúng thực sự bị trùng lặp (nghĩa là chúng xuất hiện hai lần), bạn có thể sử dụng cột "ẩn" ctid, như được đề xuất ở trên, cùng với LIMIT:

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

Thao tác này sẽ chỉ xóa hàng đầu tiên trong số các hàng đã chọn.


Tôi biết nó không giải quyết vấn đề của OP, người có nhiều bản sao trong hàng triệu hàng, nhưng nó có thể hữu ích dù sao.
Skippy le Grand Gourou

Điều này sẽ phải được chạy một lần cho mỗi hàng trùng lặp. câu trả lời của shekwi chỉ cần được chạy một lần.
bradw2k

3

Trước tiên, bạn cần phải quyết định xem bạn sẽ giữ "bản sao" nào. Nếu tất cả các cột đều bằng nhau, OK, bạn có thể xóa bất kỳ cột nào trong số chúng ... Nhưng có lẽ bạn chỉ muốn giữ lại tiêu chí gần đây nhất hoặc một số tiêu chí khác?

Cách nhanh nhất phụ thuộc vào câu trả lời của bạn cho câu hỏi ở trên, và cũng dựa trên% số trùng lặp trên bảng. Nếu bạn loại bỏ 50% số hàng của mình, thì tốt hơn là bạn đang làm CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;và nếu bạn xóa 1% số hàng, sử dụng DELETE sẽ tốt hơn.

Ngoài ra đối với các hoạt động bảo trì như thế này, nói chung bạn nên đặt work_memthành một phần RAM tốt: chạy EXPLAIN, kiểm tra số N sắp xếp / băm và đặt work_mem thành RAM / 2 / N. Sử dụng nhiều RAM; nó tốt cho tốc độ. Miễn là bạn chỉ có một kết nối đồng thời ...


1

Tôi đang làm việc với PostgreSQL 8.4. Khi tôi chạy mã được đề xuất, tôi thấy rằng nó không thực sự xóa các bản sao. Trong khi chạy một số thử nghiệm, tôi nhận thấy rằng việc thêm "DISTINCT ON (tên_mã_bản_bản)" và "ĐẶT HÀNG THEO tên_mục_sản_phẩm" đã thực hiện thủ thuật. Tôi không phải là chuyên gia SQL, tôi đã tìm thấy điều này trong tài liệu 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

Điều này hoạt động rất độc đáo và rất nhanh chóng:

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

Xóa các bản sao theo (các) cột và giữ hàng có id thấp nhất. Mẫu được lấy từ wiki postgres

Sử dụng CTE, bạn có thể đạt được phiên bản dễ đọc hơn của phần trên thông qua việc này

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

Tôi đã thử nghiệm nó, và nó hoạt động; Tôi đã định dạng nó để dễ đọc. Nó trông khá phức tạp, nhưng nó có thể sử dụng một số giải thích. Làm thế nào một người sẽ thay đổi ví dụ này cho trường hợp sử dụng của riêng mình?
Tobias
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.