datarekha
SQL Medium Asked at AmazonAsked at Microsoft

How do you delete duplicate rows from a table using ROW_NUMBER, keeping only one copy per duplicate group?

The short answer

Assign ROW_NUMBER() partitioned by the columns that define a duplicate and ordered by a tiebreaker (e.g., primary key or created_at). Any row where the row number exceeds 1 is a duplicate — delete those rows via a CTE or subquery referencing the physical row identifier.

How to think about it

The approach is two mirrored steps: first identify duplicates, then act on them. Always do the labelling step first — confirm you’re marking the right rows before anything is deleted, because a DELETE you got wrong is expensive to undo.

PARTITION BY defines what makes two rows “the same record”; the window’s ORDER BY decides which copy to keep (lowest id = earliest, highest = most recent).

Step 1 — label every row

SELECT id, email, created_at,
       ROW_NUMBER() OVER (
         PARTITION BY email      -- "same" means same email
         ORDER BY id ASC         -- keep the earliest row
       ) AS rn
FROM users
ORDER BY email, id;
idemailcreated_atrn
1alice@example.com2024-01-011
3alice@example.com2024-01-052
6alice@example.com2024-01-083
2bob@example.com2024-01-021
5bob@example.com2024-01-072
4carol@example.com2024-01-061

Each email’s window restarts the count: rn = 1 is the keeper (the earliest id), and every rn > 1 is a duplicate to remove — here ids 3, 6, and 5. Flip the window to ORDER BY id DESC and you’d keep the latest instead. The labelling makes the deletion target unambiguous before you touch the data.

Step 2 — delete the marked rows

In PostgreSQL you can DELETE straight from a CTE:

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

MySQL forbids referencing the target table directly in the subquery, so wrap it in one more level to force materialisation:

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

For a multi-million-row table, prefer copying the keepers into a fresh table and swapping, rather than a giant in-place DELETE: CREATE TABLE users_clean AS SELECT ... WHERE rn = 1, verify the counts, then rename.

Learn it properly Deduplication

Keep practising

All SQL questions

Explore further

Skip to content