Skip to content
datarekha

Deduplication

Duplicates in production data are the rule, not the exception. Two SQL patterns to remove them — and the judgement call that decides which row survives.

Written and reviewed by Last reviewed

6 min read Intermediate SQL Lesson 16 of 27

What you'll learn

  • Two patterns — ROW_NUMBER + filter, and GROUP BY with an aggregate
  • Deciding which duplicate is 'the right one' to keep
  • Why SELECT DISTINCT usually isn't enough
  • Finding the duplicates before you delete anything

Before you start

Production data has duplicates. A frontend retried a request, a background job ran twice, or an ETL load was rerun without an idempotency key. Whatever the cause, the job is the same: collapse the duplicates without losing information.

SELECT DISTINCT is the wrong tool most of the time. It only removes rows that are identical in every selected column. Real duplicates differ in something — a fresh id, a slightly later timestamp. So before any SQL, you must decide two things: what counts as a duplicate, and which copy to keep. Take this orders table, where the same purchase was recorded twice:

iduser_idproduct_idamount
174900
274900
3939

Rows 1 and 2 are the same (user_id, product_id) — a duplicate group. SELECT DISTINCT * would keep both, because their ids differ.

Pattern 1 — ROW_NUMBER, then keep rn = 1

Duplicate rows grouped by key, with one kept per key and the rest discarded.

Decide the key, decide which one wins, then keep exactly that.

The flexible pattern: number the rows within each “should be unique” group, ordered by which one wins, and keep the first:

WITH ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY user_id, product_id ORDER BY id) AS rn
  FROM   orders
)
SELECT id, user_id, product_id, amount
FROM   ranked
WHERE  rn = 1;
iduser_idproduct_idamount
174900
3939

PARTITION BY user_id, product_id defines the duplicate group, and ORDER BY id picks the survivor — here, the lowest id, the first one inserted. Row 2 loses and drops out. This keeps the whole row, which is its advantage over the next pattern.

Pattern 2 — GROUP BY with an aggregate

When you only need the keys plus a summary, GROUP BY is tighter. But it collapses the row to keys and aggregates, so you cannot get the full surviving row back this way:

SELECT user_id, COUNT(*) AS n_rows, MAX(amount) AS max_amount
FROM   orders
GROUP BY user_id;
user_idn_rowsmax_amount
72900
919
Use ROW_NUMBER when…Use GROUP BY when…
You need the full rowYou only need keys + aggregates
There’s a clear winner (latest, lowest, highest)Duplicates are exact, either is fine
You may want to inspect the losersYou just want unique values + stats

Look before you delete

Before running anything destructive, find the duplicates with the standard “show me the dupes” query:

SELECT user_id, product_id, COUNT(*) AS dup_count
FROM   orders
GROUP BY user_id, product_id
HAVING COUNT(*) > 1;
user_idproduct_iddup_count
742

Practice

Quick check

0/3
Q1Two events share user_id and event_id but have different timestamps. What does SELECT DISTINCT * do?
Q2You want the latest event per user_id and need the full row, all columns. Which pattern?
Q3Best thing to write before a deduplication query?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions

Related lessons

Explore further