Skip to content

Grokem · https://www.grokem.com/data/data-management/data-quality/remove-duplicate-rows-sql

Data QualityHow-to guide

Find and remove duplicate rows in SQL

Count the copies, number them with ROW_NUMBER, delete all but one per group, and add a unique index so they cannot come back.

Grokem Editorial

Editorial desk

Published
  • Beginner
  • 10 min to complete
Tested withSQLite3.42

Before you start

  • A table with a column (or set of columns) that should be unique
  • A backup or a transaction you can roll back: this deletes data

Duplicates usually arrive through a retried import, a form submitted twice or two systems feeding one table. This guide removes them safely and then stops them returning. The examples use a small customers table.

sql
CREATE TABLE customers (id INTEGER PRIMARY KEY, email TEXT, name TEXT, created_at TEXT);
INSERT INTO customers (email, name, created_at) VALUES
 ('ana@example.com', 'Ana',   '2025-01-03'),
 ('ana@example.com', 'Ana B', '2025-02-10'),
 ('ben@example.com', 'Ben',   '2025-01-20'),
 ('cy@example.com',  'Cy',    '2025-01-25'),
 ('cy@example.com',  'Cy',    '2025-01-25'),
 ('cy@example.com',  'Cyrus', '2025-03-01');

1. Find the duplicates

Group by whatever should be unique and keep only the groups with more than one row.

sql
SELECT email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY email;

This returns ana@example.com with 2 copies and cy@example.com with 3. Run it first and read the result: it tells you whether you are about to touch 5 rows or 5 million.

2. Decide which copy survives, and number them

ROW_NUMBER() gives each row in a group a position. The ORDER BY inside the window is your rule: here the newest created_at wins, and id breaks a tie so the result is the same every time.

sql
SELECT id, email, name, created_at,
       ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY created_at DESC, id DESC
       ) AS rn
FROM customers
ORDER BY email, rn;

The row with rn = 1 in each group is the one you keep. For cy@example.com that is the March row, Cyrus.

3. Delete everything else

Wrap the numbering in a subquery and delete the ids whose rn is greater than 1. Do this inside a transaction so you can check the result before you commit.

sql
BEGIN;

DELETE FROM customers
WHERE id IN (
    SELECT id FROM (
        SELECT id,
               ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC, id DESC) AS rn
        FROM customers
    )
    WHERE rn > 1
);

SELECT id, email, name, created_at FROM customers ORDER BY email;
-- happy with it? COMMIT;   otherwise: ROLLBACK;

After the delete, three rows remain: Ana B (id 2), Ben (id 3) and Cyrus (id 6).

4. Make it impossible to happen again

Cleaning up is only half the job. A unique index makes the database refuse the next duplicate.

sql
CREATE UNIQUE INDEX customers_email_unique ON customers (email);

Inserting ben@example.com again now fails with UNIQUE constraint failed: customers.email. Your import code should catch that error and decide what to do, for example update the existing row instead of adding a new one.

Things to watch

  • Case and spaces. Ana@Example.com and ana@example.com are different strings. Clean them first (LOWER(TRIM(email))) or the duplicates will not be found.
  • Foreign keys. If other tables point at the rows you delete, re-point them to the surviving id first.
  • NULLs. Most databases allow several NULLs in a unique column, so rows with no email are not treated as duplicates of each other.

Expected result

Each email appears once, the newest row survives, and inserting the same email again is refused.

Frequently asked questions

Does this work in PostgreSQL, MySQL and SQL Server?

ROW_NUMBER() OVER (PARTITION BY ...) is standard SQL and works in all current versions of them. The DELETE ... WHERE id IN (subquery) shape is portable too; only quoting and date types differ.

What if I cannot tell which duplicate is the right one?

Then you need a rule before you need a query. Newest, most complete or from the most trusted source are common rules. Write the rule in the ORDER BY.

References

From Grokem: https://www.grokem.com/data/data-management/data-quality/remove-duplicate-rows-sql

How did this strike you?

Be the first to react – it is anonymous.

Community

Questions about this?

Ask in Questions and help and get help from other readers.

Ask a questionBrowse Questions and help

Comments (2)

  • Chloé

    When two rows are exactly identical, which one does the ROW_NUMBER approach keep?

    • Grokem Editorial

      Whichever comes first in the `ORDER BY` inside the window. If two rows tie there as well, the database may pick either, and a re-run could pick the other. Add a tie-breaker, such as the primary key, so the same row survives every time.

Choose what we may use. You can change this at any time with “Privacy choices” at the foot of every page. Details are in the Cookie policy and the Privacy policy.

Strictly necessary

Remembers your theme, the lessons you marked as done and this choice. It stays on your device, tracks nothing and cannot be switched off.

Always on
What exactly is collected, and why

Grokem’s own counts

Collects
The pages you open and how long you stay, how far you scroll, where you came from, your device type, browser, screen size, language and time zone. Your IP address is turned into a code that changes every day; it is never stored as it is.
Why
To see how long lessons are read and where readers leave. Not shared with anyone.
Stored
A random visitor ID, a visit ID and the time of your last page view in your browser’s storage (gr_vid, gr_sid, gr_last). No name or email is attached.
Find and remove duplicate rows in SQL | Grokem