Finding and Deleting Duplicates Without Losing the Original

Intermediate 5 min read Updated 30 Jul 2026 UPA AI Partner

Four rows, two customers. One wrong statement removes both. Here is how to delete safely.

The problem

The users table has duplicate emails from multiple import sources. You need to know which emails repeat, and remove the extras.

NGUOI_DUNGidemail1an@x.com2binh@x.com3an@x.com4an@x.comBẢN TRÙNGemailso_lanid_giuan@x.com31Giữ id nhỏ nhất, xoá 3 và 4GROUP BY + HAVING

Step 1 — find the duplicated values

SELECT email, COUNT(*) AS times
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

This is a fixed pattern: group by the column you are checking, keep groups with more than one row.

Step 2 — see the full duplicate rows

SELECT *
FROM users
WHERE email IN (
  SELECT email FROM users
  GROUP BY email HAVING COUNT(*) > 1
)
ORDER BY email, id;

Step 3 — delete, keeping one

Option 1: keep the lowest id

DELETE FROM users
WHERE id NOT IN (
  SELECT MIN(id) FROM users GROUP BY email
);

Option 2: ROW_NUMBER — more flexible

WITH numbered AS (
  SELECT id,
    ROW_NUMBER() OVER (
      PARTITION BY email ORDER BY created_at ASC, id ASC
    ) AS rn
  FROM users
)
DELETE FROM users
WHERE id IN (SELECT id FROM numbered WHERE rn > 1);

This version lets you decide which copy survives — the oldest, the most complete, whatever your criterion is.

Always run the SELECT first Change DELETE FROM to SELECT * FROM, read the list carefully, then change it back. There is no undo.

Common mistakes

1. Matching on unnormalised data

'An@X.com ' and 'an@x.com' are different values. Group on LOWER(TRIM(email)) to catch them all.

2. Forgetting MySQL cannot DELETE from the table it is selecting

-- Error 1093 on MySQL
DELETE FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY email);

-- Wrap one level deeper to work around it
DELETE FROM users WHERE id NOT IN (
  SELECT * FROM (SELECT MIN(id) FROM users GROUP BY email) t
);

3. Not preventing the next round

CREATE UNIQUE INDEX ux_users_email ON users(LOWER(email));

Clean the data without adding a constraint and you will be doing this again next week.

Practice

  • LeetCode 182 — Duplicate Emails
  • LeetCode 196 — Delete Duplicate Emails
  • LeetCode 1050 — Actors and Directors Who Cooperated At Least Three Times
  • LeetCode 1729 — Find Followers Count

Key takeaways

  • GROUP BY … HAVING COUNT(*) > 1 is the standard duplicate-finding pattern.
  • ROW_NUMBER lets you choose which copy to keep.
  • Always run it as a SELECT before turning it into a DELETE.
  • Normalise strings before matching.
  • Add a UNIQUE INDEX so you never repeat the exercise.

You have just learned tìm và xoá dữ liệu trùng

Ready to practise?

Work through tìm và xoá dữ liệu trùng exercises on real data, graded the moment you hit run.

Start practising →