For me, the WMPL support post mentioned by millend was not relevant except to give me a clue which tables to look at. The WPML support post was about removing translations (…WHERE language_code != ‘en’), whereas the bad entries in wp_icl_translations in my case all had language_code=’en’. But I figured out that a distinctive of almost all the junk wp_posts entries were that their id’s were not in wp_postmeta.
First I looked in the Media Library and filtered by “Unattached” and “September 2014” (I didn’t have any in August, so that worked for me). Noting the number of entries, I then did this query:
SELECT * FROM wp_posts WHERE post_type='attachment' AND id NOT IN (SELECT post_id FROM wp_postmeta)
The number was slightly lower, but good enough for me, as I knew I could clean up the last few manually in Media Library. To verify which “good” entries those are duplicates of, this works as long as you haven’t started deleting anything manually:
SELECT min(id)-1, post_title FROM wp_posts where post_type='attachment' and id not in (select post_id from wp_postmeta) group by post_title
WPML had inserted two related records in wp_icl_translations for each id in wp_posts – a comment and a post_attachment. To see the ones that would be deleted later, you can use this nested subquery:
SELECT * FROM wp_icl_translations WHERE element_id IN (SELECT id FROM wp_posts WHERE post_type='attachment' AND id NOT IN (SELECT post_id FROM wp_postmeta))
When you are satisfied that you’re looking at the right records to delete (AND YOU HAVE DONE A BACKUP!), you delete from wp_icl_translations first:
DELETE FROM wp_icl_translations WHERE element_id IN (SELECT id FROM wp_posts WHERE post_type='attachment' AND id NOT IN (SELECT post_id FROM wp_postmeta))
Then you can remove the wp_posts entries themselves:
DELETE FROM wp_posts WHERE post_type='attachment' AND id NOT IN (SELECT post_id FROM wp_postmeta)