{"id":1744,"date":"2009-04-05T12:08:45","date_gmt":"2009-04-05T16:08:45","guid":{"rendered":"http:\/\/blogs.n1zyy.com\/n1zyy\/?p=1744"},"modified":"2009-04-05T12:08:45","modified_gmt":"2009-04-05T16:08:45","slug":"removing-duplicates-from-mysql","status":"publish","type":"post","link":"https:\/\/blogs.n1zyy.com\/n1zyy\/2009\/04\/05\/removing-duplicates-from-mysql\/","title":{"rendered":"Removing Duplicates from MySQL"},"content":{"rendered":"<p>I Googled that a while ago. How do you remove duplicate rows from a SQL database? (MySQL specifically.)<\/p>\n<p>If you have a &#8216;simple&#8217; duplicate setup, it&#8217;s pretty easy, and lots of other sites go into great detail. (And in that case, you might look into how to require that fields be unique so you don&#8217;t have that problem anymore&#8230;)<\/p>\n<p>In our case, we ran into an obscure problem where we expected the combination of two fields to be unique. Without publicizing anything secret, we had a summary table of user_id and month. (Not that it&#8217;s actually anything secret, but I refer to the table I did this on as &#8220;tablename,&#8221; just since blogging about our production database schemas seems like a bad idea&#8230;) A brief bug introduced some duplicates. My task? Remove the duplicate rows.<\/p>\n<p>So I went about it somewhat backwards, and started wondering how do a SELECT DISTINCT across two columns. You can&#8217;t, really. Someone recommended something clever: CONCAT() across the two field names, and a DISTINCT on that: SELECT DISTINCT(CONCAT(user_id, month))&#8230; After toying with that a bit, I was able to create a temporary table housing a de-duped version of our data:<\/p>\n<blockquote><tt>\ncreate temporary table nodupes\nSELECT DISTINCT(CONCAT(user_id, month)) asdf, tablename.*\nFROM tablename GROUP BY asdf;\n<\/tt><\/blockquote>\n<p>But how does that help? The problem with temporary tables is that they&#8217;re, uhh, temporary. If you lived on the edge, you could probably &#8220;DROP TABLE original_table; CREATE TABLE original_table (SELECT * FROM temp_table)&#8221;, but this has a few <em>horrible<\/em> problems. One is that you&#8217;re throwing away all your disk-based data so you can populate it with a version that exists only in RAM. If anything went wrong, you&#8217;d lose the data. Furthermore, in our case, this is a live, busy table. Dropping it in production is awful, and it would take many minutes to repopulate it since it&#8217;s a huge table.<\/p>\n<p>So then it was clear what I wanted to do: something like DELETE FROM original_table WHERE id NOT IN(SELECT * FROM temp_table_with_no_dupes), except not using an IN() on a table with hundreds of thousands of rows. And this is where it got tricky. The key is to use that temporary table to derive a new temporary table, housing only the duplicates. (Actually, speaking of &#8220;the key,&#8221; create one in your temporary table: <tt>CREATE INDEX gofast ON nodupes(id);<\/tt> &#8212; you&#8217;ll need this soon, as it&#8217;s the difference between the next query taking 10 seconds and me aborting it after 30 minutes.)<\/p>\n<p>The temporary table of duplicate rows is basically derived by joining your &#8216;real&#8217; table with the list of de-duped tables, with a rather obscure trick: joining where id=NULL. So here&#8217;s what I did:<\/p>\n<blockquote><tt>\nCREATE TEMPORARY TABLE dupes\n(SELECT tablename.*\nFROM tablename\nLEFT OUTER JOIN nodupes nd\nON nd.id=tablename.id WHERE nd.id IS NULL);\n<\/tt><\/blockquote>\n<p>For bonus points, we&#8217;re selecting from the real table, so the silly CONCAT() field doesn&#8217;t come through into this temporary table.<\/p>\n<p>So once again, I momentarily flashed a sly grin. This is going to be easy! <tt>DELETE FROM tablename WHERE id IN(SELECT id FROM dupes);<\/tt>. But then my sly grin faded. It turns out that there are two problems here. The first is that there are still a lot of rows. In my case, I was doing an IN() with about 6,000 rows. MySQL doesn&#8217;t like that. But there&#8217;s a bigger problem. (Thanks to <a href=\"http:\/\/blogs.n1zyy.com\/andrew\/\">Andrew<\/a> for confirming that this is a real bug: <a href=\"http:\/\/bugs.mysql.com\/bug.php?id=9090\">#9090<\/a>.) MySQL doesn&#8217;t use indexes when you use a subquery like that. It seems the bug is pretty narrow in scope, but this is precisely it. So that query is <em>awful<\/em>, basically doing 6,000 full table scans. And on a really big table.<\/p>\n<p>So the solution comes from something I&#8217;ve never even thought about doing before. MySQL calls it the &#8220;multi-delete.&#8221; I call it the, &#8220;Holy smokes, this could go really, really wrong if I&#8217;m not careful&#8221; delete.<\/p>\n<blockquote><tt>\n-- Don't run this yet\nDELETE bb\nFROM tablename AS bb\nRIGHT JOIN dupes d\nON (d.id=bb.id);\n<\/tt><\/blockquote>\n<p>That&#8217;s the command you want, but a few caveats. First, change the first line to &#8220;SELECT *&#8221; and run it to see what happens. Make sure it&#8217;s only grabbing the duplicate rows. You&#8217;re not able to use a LIMIT in a multidelete query, so you have to be extremely careful here. Second, we tested this multiple times on various test databases before doing it against our master database, and I was still uneasy when it came time to hit enter. Keep backups before you do this. And finally, in many cases, MySQL will expect an explicit &#8220;USE database_name&#8221; before the multi-delete, where database_name is the database housing the table you&#8217;re deleting from. Keep that in mind if you get an error that it can&#8217;t find the table, even if you explicitly reference it (via database_name.table_name).<\/p>","protected":false},"excerpt":{"rendered":"<p>I Googled that a while ago. How do you remove duplicate rows from a SQL database? (MySQL specifically.) If you have a &#8216;simple&#8217; duplicate setup, it&#8217;s pretty easy, and lots of other sites go into great detail. (And in that &hellip; <a href=\"https:\/\/blogs.n1zyy.com\/n1zyy\/2009\/04\/05\/removing-duplicates-from-mysql\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,13,22],"tags":[],"class_list":["post-1744","post","type-post","status-publish","format-standard","hentry","category-computers","category-linux-tips","category-programming"],"_links":{"self":[{"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/posts\/1744","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/comments?post=1744"}],"version-history":[{"count":0,"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/posts\/1744\/revisions"}],"wp:attachment":[{"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/media?parent=1744"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/categories?post=1744"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.n1zyy.com\/n1zyy\/wp-json\/wp\/v2\/tags?post=1744"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}