Làm cách nào để xóa các bản ghi trùng lặp trong bảng tham gia trong PostgreSQL?


9

Tôi có một bảng có một lược đồ như thế này:

create_table "questions_tags", :id => false, :force => true do |t|
        t.integer "question_id"
        t.integer "tag_id"
      end

      add_index "questions_tags", ["question_id"], :name => "index_questions_tags_on_question_id"
      add_index "questions_tags", ["tag_id"], :name => "index_questions_tags_on_tag_id"

Tôi muốn xóa các bản ghi là bản sao, tức là chúng có cả bản ghi giống nhau tag_idquestion_idbản ghi khác.

SQL trông như thế nào cho điều đó?

Câu trả lời:


15

Theo kinh nghiệm của tôi (và như được thể hiện trong nhiều thử nghiệm) NOT INnhư được chứng minh bởi @gsiems là khá chậm và quy mô khủng khiếp. Nghịch đảo INthường nhanh hơn (nơi bạn có thể định dạng lại theo cách đó, như trong trường hợp này), nhưng truy vấn này với EXISTS(thực hiện chính xác những gì bạn yêu cầu) nên nhanh hơn nhiều - với các bảng lớn theo thứ tự độ lớn :

DELETE FROM questions_tags q
WHERE  EXISTS (
   SELECT FROM questions_tags q1
   WHERE  q1.ctid < q.ctid
   AND    q1.question_id = q.question_id
   AND    q1.tag_id = q.tag_id
   );

Xóa mọi hàng trong đó một hàng khác có cùng (tag_id, question_id)và nhỏ hơn ctidtồn tại . (Hiệu quả giữ phiên bản đầu tiên theo thứ tự vật lý của bộ dữ liệu.) Sử dụng ctidtrong trường hợp không có giải pháp thay thế tốt hơn, bảng của bạn dường như không có PK hoặc bất kỳ cột (bộ) duy nhất nào khác.

ctidlà định danh tuple nội bộ có mặt trong mỗi hàng và nhất thiết phải là duy nhất. Đọc thêm:

Kiểm tra

Tôi đã chạy một trường hợp thử nghiệm với bảng này khớp với câu hỏi của bạn và hàng 100k:

CREATE TABLE questions_tags(
  question_id integer NOT NULL
, tag_id      integer NOT NULL
);

INSERT INTO questions_tags (question_id, tag_id)
SELECT (random()* 100)::int, (random()* 100)::int
FROM   generate_series(1, 100000);

ANALYZE questions_tags;

Chỉ mục không giúp đỡ trong trường hợp này.

Các kết quả

NOT IN
Các SQLfiddle lần ra ngoài.
Đã thử tương tự tại địa phương nhưng tôi cũng đã hủy nó sau vài phút.

EXISTS
Kết thúc sau nửa giây trong SQLfiddle này .

Lựa chọn thay thế

Nếu bạn định xóa hầu hết các hàng , sẽ nhanh hơn khi chọn những người sống sót vào một bảng khác, bỏ bản gốc và đổi tên bảng của người sống sót. Cẩn thận, điều này có ý nghĩa nếu bạn có chế độ xem hoặc khóa ngoại (hoặc các phụ thuộc khác) được xác định trên bản gốc.

Nếu bạn có sự phụ thuộc và muốn giữ chúng, bạn có thể:

  • Thả tất cả các khóa và chỉ mục nước ngoài - cho hiệu suất.
  • SELECT những người sống sót đến một bảng tạm thời.
  • TRUNCATE bản gốc.
  • Những INSERTngười sống sót.
  • Chỉ CREATEsố lại và khóa ngoại. Lượt xem chỉ có thể ở lại, chúng không có tác động đến hiệu suất. Thêm ở đây hoặc ở đây .

++ cho giải pháp tồn tại. Tốt hơn nhiều so với đề nghị của tôi.
gsiems

Bạn có thể vui lòng giải thích so sánh ctid trong mệnh đề WHERE của bạn?
Kevin Meredith

1
@KevinMeredith: Tôi đã thêm một số lời giải thích.
Erwin Brandstetter

6

Bạn có thể sử dụng ctid để thực hiện điều đó. Ví dụ:

Tạo một bảng với các bản sao:

=# create table foo (id1 integer, id2 integer);
CREATE TABLE

=# insert into foo values (1,1), (1, 2), (1, 2), (1, 3);
INSERT 0 4

=# select * from foo;
 id1 | id2 
-----+-----
   1 |   1
   1 |   2
   1 |   2
   1 |   3
(4 rows)

Chọn dữ liệu trùng lặp:

=# select foo.ctid, foo.id1, foo.id2, foo2.min_ctid
-#  from foo
-#  join (
-#      select id1, id2, min(ctid) as min_ctid 
-#          from foo 
-#          group by id1, id2 
-#          having count (*) > 1
-#      ) foo2 
-#      on foo.id1 = foo2.id1 and foo.id2 = foo2.id2
-#  where foo.ctid <> foo2.min_ctid ;
 ctid  | id1 | id2 | min_ctid 
-------+-----+-----+----------
 (0,3) |   1 |   2 | (0,2)
(1 row)

Xóa dữ liệu trùng lặp:

=# delete from foo
-# where ctid not in (select min (ctid) as min_ctid from foo group by id1, id2);
DELETE 1

=# select * from foo;
 id1 | id2 
-----+-----
   1 |   1
   1 |   2
   1 |   3
(3 rows)

Trong trường hợp của bạn, những điều sau đây nên hoạt động:

delete from questions_tags
    where ctid not in (
        select min (ctid) as min_ctid 
            from questions_tags 
            group by question_id, tag_id
        );

Tôi có thể đọc thêm về điều này ở ctidđâu? Cảm ơn.
marcamillion

@marcamillion - Tài liệu này có một đoạn giới thiệu ngắn về ctids tại postgresql.org/docs/civerse/static/ddl-system-columns.html
gsiems

Không đại diện ctidcho cái gì?
marcamillion

@marcamillion - tid == "tuple id", không chắc c nghĩa là gì.
gsiems
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.