Phase 8 · SQL interview problems, from easy to hard · Topic 8.6

Find duplicate emails, then remove the duplicates

Must knowInterview problemEasy

Find every email that belongs to more than one customer, with how many sign-ups it has. Then delete the duplicates, keeping the row with the lowest customer_id.

Not started

Step 1 Solve it yourself first

Tables you will use

customers

What it really tests

with , the pattern for picking one per group, and writing a safe .

The pattern: Duplicates (GROUP BY … HAVING (*) > 1)

"Duplicate", "appears more than once", "keep only one", "de-duplicate".

Picture it

flowchart LR
  A["customers grouped by email"] --> T["tom@mail.com<br/>id 2 · id 6"]
  A --> O["other emails<br/>one row each"]
  T -->|"COUNT is 2"| D["duplicate:<br/>keep id 2, remove id 6"]
Grouping by email puts both of Tom's sign-ups in one group. A group of two is a duplicate; keep the lowest id and remove the rest.

The approach, step by step

  1. Group by the that should be unique (email).
  2. Keep groups with more than one row: HAVING COUNT(*) > 1.
  3. To see the extra rows themselves, number the rows in each email group by customer_id and keep row numbers above 1.
  4. To delete them, keep the MIN(customer_id) per email and delete everything else. Run it as a first.

Your turn: write it before you look

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.
Show the solution

Stuck for more than ten minutes? Open it, read it, close it, and write it again from memory.

Solution: find them with GROUP BY and HAVING

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

1 row

emailsign_ups
tom@mail.com2

Another way: Show the extra rows with ROW_NUMBER

sql
WITH numbered AS (
  SELECT customer_id, name, email,
         ROW_NUMBER() OVER (PARTITION BY email ORDER BY customer_id) AS rn
  FROM customers
)
SELECT customer_id, name, email
FROM numbered
WHERE rn > 1
ORDER BY customer_id;

1 row

customer_idnameemail
6Tomtom@mail.com

These are the rows to remove: every row except the first in its email group.

Another way: Delete the duplicates, keeping the lowest id

sql
DELETE FROM customers
WHERE customer_id NOT IN (
  SELECT MIN(customer_id)
  FROM customers
  GROUP BY email
);
SELECT customer_id, name, email
FROM customers
ORDER BY customer_id;

5 rows

customer_idnameemail
1Priyapriya@mail.com
2Tomtom@mail.com
3Lenalena@mail.com
4Omaromar@mail.com
5Zoezoe@mail.com

is safe here because MIN(customer_id) is never . Then add a on email so it can't happen again.

Edge cases to say out loud

  • Case and spaces: 'Tom@mail.com ' and 'tom@mail.com' are different — with LOWER(TRIM(email)) if they should match.
  • NULL emails group together in GROUP BY: decide whether they count as duplicates.
  • If the duplicate rows have orders, the blocks the delete — re-point those orders to the kept id first.

Common mistakes

  • Using COUNT(*) > 1 instead of HAVING.
  • Deleting without checking the rows first with a SELECT.
  • Fixing the data but not adding a UNIQUE constraint, so duplicates come straight back.

In other databases

  • can't delete from a while selecting from it in a ; wrap the subquery in another SELECT or use a DELETE … .

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.

Step 3 Practise

Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.

  1. ExplainSay what it does and when to use it, in plain words, with no notes.

    Explain why HAVING and not WHERE is used to find duplicates.

    Show answer

    A duplicate email is a fact about a group of rows ("this email appears more than once"), not about any single row. Looking at one row on its own, you can't tell whether its email is repeated.

    WHERE runs before GROUP BY, one row at a time, so COUNT(*) doesn't exist yet there. GROUP BY email makes one group per email, and HAVING runs after it, so HAVING COUNT(*) > 1 can keep only the groups with more than one row.

  2. Writefrom a blank page, then run it

    Find customers who share the same name and city, and how many rows each pair has.

    Show answer

    Group by both columns and keep groups with more than one row. Only Tom in Leeds appears twice.

    Note: GROUP BY puts NULLs together, so two people with the same name and no city would count as a pair. Usually that is what you want for finding duplicates, but say it out loud.

    sql
    SELECT name, city, COUNT(*) AS sign_ups
    FROM customers
    GROUP BY name, city
    HAVING COUNT(*) > 1
    ORDER BY name, city;

    1 row

    namecitysign_ups
    TomLeeds2
  3. Predictsay it before you run it

    What would the first return if Zoe's email were changed to 'priya@mail.com'?

    Show answer

    Two rows: priya@mail.com with 2 sign-ups (Priya and now Zoe), and tom@mail.com with 2, as before. The query doesn't care that the names differ; it only groups by email.

    sql
    UPDATE customers SET email = 'priya@mail.com' WHERE name = 'Zoe';
    SELECT email, COUNT(*) AS sign_ups
    FROM customers
    GROUP BY email
    HAVING COUNT(*) > 1
    ORDER BY email;

    2 rows

    emailsign_ups
    priya@mail.com2
    tom@mail.com2
  4. Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.

    Rewrite the duplicate check so 'TOM@mail.com' and 'tom@mail.com' count as the same email.

    Show answer

    Group by a cleaned-up version of the email: LOWER(TRIM(email)). makes the case the same; TRIM removes stray spaces, another common cause of fake "different" emails. Below, Tom's second sign-up is changed to 'TOM@mail.com' and the two still count as one.

    To stop it happening again, store emails lower-cased, and put a unique on the lower-cased value (CREATE UNIQUE INDEX … ON customers (LOWER(email)), or a COLLATE NOCASE column in ).

    sql
    UPDATE customers SET email = 'TOM@mail.com' WHERE customer_id = 6;
    SELECT LOWER(TRIM(email)) AS email, COUNT(*) AS sign_ups
    FROM customers
    GROUP BY LOWER(TRIM(email))
    HAVING COUNT(*) > 1
    ORDER BY 1;

    1 row

    emailsign_ups
    tom@mail.com2

Practice box: write and run your answers here

Ctrl/⌘ + Enter runs it. Select part of the query to run only that part.

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud.

To find duplicates I group by the column that should be unique and keep groups with HAVING COUNT star greater than one. To remove them I number the rows within each group with ROW_NUMBER — or take the minimum id per group — and delete every row that isn't the keeper, checking it as a SELECT first. Then I'd add a unique constraint, and I'd ask whether emails need normalising for case and spaces before comparing.

Follow-up questions

How would you keep the most recent sign-up instead?

Order the ROW_NUMBER by signup_date (with customer_id as a ), or keep MAX(customer_id) if ids grow over time.

What if the duplicate customer has orders?

Update those orders to point at the kept customer_id first, in the same , then delete.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.

  • Explain Say what it does and when to use it, in plain words, with no notes.
  • Write Write the query from a blank page, then run it to check.
  • Predict Read a query and say which rows it returns before you run it.
  • Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.