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.

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, 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, because it only removes rows that are identical in every selected column — and 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

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

Skip to content