I have a table that has a schema like this:
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"
I would like to remove records that are duplicates, i.e. they have both the same tag_id
and question_id
as another record.
What does the SQL look like for that?
2 Answers 2
In my experience (and as shown in many tests) NOT IN
as demonstrated by @gsiems is rather slow and scales terribly. The inverse IN
is typically faster (where you can reformulate that way, like in this case), but this query with EXISTS
(doing exactly what you asked) should be much faster yet - with big tables by orders of magnitude:
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
);
Deletes every row where another row with the same (tag_id, question_id)
and a smaller ctid
exists. (Effectively keeps the first instance according to the physical order of tuples.) Using ctid
in the absence of a better alternative, your table does not seem to have a PK or any other unique (set of) column(s).
ctid
is the internal tuple identifier present in every row and necessarily unique within a single table. You need to do more if multiple tables can be involved under the hood, like with inheritance or partitioning. See:
Further reading:
- How do I decompose ctid into page and row numbers?
- How list all tables with data changes in the last 24 hours?
- How do I (or can I) SELECT DISTINCT on multiple columns?
Test
I ran a test case with this table matched to your question and 100k rows:
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;
Indexes do not help in this case.
Results
NOT IN
The SQLfiddle times out.
Tried the same locally but I canceled it, too, after several minutes.
EXISTS
Finishes in half a second in this SQLfiddle.
Alternatives
If you are going to delete most of the rows, it will be faster to select the survivors into another table, drop the original and rename the survivor's table. Careful, this has implications if you have view or foreign keys (or other dependencies) defined on the original.
If you have dependencies and want to keep them, you could:
-
++ for the exists solution. Much better than my suggestion.gsiems– gsiems2013年03月14日 01:53:51 +00:00Commented Mar 14, 2013 at 1:53
-
Could you please explain the ctid comparison in your WHERE clause?Kevin Meredith– Kevin Meredith2019年09月08日 12:39:21 +00:00Commented Sep 8, 2019 at 12:39
-
1@KevinMeredith: I added some explanation.Erwin Brandstetter– Erwin Brandstetter2019年09月08日 22:24:42 +00:00Commented Sep 8, 2019 at 22:24
You can use the ctid to accomplish that. For example:
Create a table with duplicates:
=# 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)
Select the duplicate data:
=# 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)
Delete the duplicate data:
=# 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)
In your case the following should work:
delete from questions_tags
where ctid not in (
select min (ctid) as min_ctid
from questions_tags
group by question_id, tag_id
);
-
Where can I read more about this
ctid
? Thanks.marcamillion– marcamillion2013年03月13日 09:16:43 +00:00Commented Mar 13, 2013 at 9:16 -
@marcamillion -- The documentation has a short blurb on ctids at postgresql.org/docs/current/static/ddl-system-columns.htmlgsiems– gsiems2013年03月13日 15:25:06 +00:00Commented Mar 13, 2013 at 15:25
-
What does
ctid
stand for?marcamillion– marcamillion2013年03月14日 05:40:03 +00:00Commented Mar 14, 2013 at 5:40 -
@marcamillion -- tid == "tuple id", not sure what the c means.gsiems– gsiems2013年03月14日 14:04:22 +00:00Commented Mar 14, 2013 at 14:04