By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
A Hyper-Practical, Zero-Fluff Study Guide
Duplicates in your data are like weeds in a garden—they spread fast, choke insights, and make stakeholders question your credibility. In SQL for data analysis, duplicates are rows that appear identical (or nearly identical) in key columns, often due to: - Bad ETL pipelines (e.g., a JOIN gone wrong, or a UNION without DISTINCT).- User error (e.g., double-submitted forms, manual data entry mistakes).- System glitches (e.g., a cron job running twice, or a Kafka consumer reprocessing messages).
JOIN
UNION
DISTINCT
Why this matters in production:- Broken aggregations: SUM(), COUNT(), and AVG() will inflate if duplicates exist.- Misleading dashboards: A "unique users" metric becomes "total logins" if duplicates aren’t handled.- Wasted compute: Processing 10M rows instead of 5M because of duplicates slows queries and increases cloud costs.- Compliance risks: GDPR/CCPA require accurate "right to erasure" handling—duplicates can lead to incomplete deletions.
SUM()
COUNT()
AVG()
Real-world scenario:You’re analyzing customer churn for a SaaS company. Your users table has 10K rows, but your COUNT(DISTINCT user_id) returns 8K. After digging, you find that 2K users were duplicated due to a failed migration. Your churn rate calculation is now 20% off—enough to mislead the CEO into making the wrong retention strategy.
users
COUNT(DISTINCT user_id)
This guide will teach you how to detect, handle, and prevent duplicates like a pro.
user_id
order_date
GROUP BY
MAX()
ROW_NUMBER()
PARTITION BY
HAVING
WHERE
HAVING COUNT(*) > 1
UNION ALL
id
uuid
email
ssn
order_number
-- PostgreSQL/Snowflake/BigQuery syntax CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, name VARCHAR(255), signup_date TIMESTAMP, last_login TIMESTAMP ); -- Insert some duplicates INSERT INTO users (email, name, signup_date, last_login) VALUES ('[email protected]', 'Alice', '2023-01-01', '2023-01-10'), ('[email protected]', 'Alice Smith', '2023-01-01', '2023-02-15'), -- Duplicate email ('[email protected]', 'Bob', '2023-01-02', '2023-01-12'), ('[email protected]', 'Robert', '2023-01-02', '2023-03-20'), -- Duplicate email ('[email protected]', 'Charlie', '2023-01-03', '2023-01-15');
-- Find emails that appear more than once SELECT email, COUNT(*) as duplicate_count FROM users GROUP BY email HAVING COUNT(*) > 1;
Expected output:
email | duplicate_count --------------------+---------------- [email protected] | 2 [email protected] | 2
-- Keep the row with the latest `last_login` for each email WITH ranked_users AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_login DESC) as rn FROM users ) SELECT id, email, name, signup_date, last_login FROM ranked_users WHERE rn = 1;
id | email | name | signup_date | last_login ---+---------------------+-------------+-------------------+------------------- 2 | [email protected] | Alice Smith | 2023-01-01 | 2023-02-15 4 | [email protected] | Robert | 2023-01-02 | 2023-03-20 5 | [email protected] | Charlie | 2023-01-03 | 2023-01-15
-- PostgreSQL: Update the table in-place WITH ranked_users AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_login DESC) as rn FROM users ) DELETE FROM users WHERE id IN ( SELECT id FROM ranked_users WHERE rn > 1 ); -- MySQL: Alternative approach (create a new table) CREATE TABLE users_deduped AS WITH ranked_users AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_login DESC) as rn FROM users ) SELECT id, email, name, signup_date, last_login FROM ranked_users WHERE rn = 1; -- Drop the old table and rename the new one DROP TABLE users; ALTER TABLE users_deduped RENAME TO users;
SELECT email, COUNT(*) as count FROM users GROUP BY email HAVING COUNT(*) > 1;
Expected output: (No rows returned)
SHA256
sql SELECT SHA256(email) as email_hash, COUNT(*) as count FROM users GROUP BY email_hash HAVING COUNT(*) > 1;
SELECT *
LIMIT
sql SELECT email, COUNT(*) as count FROM users GROUP BY email HAVING COUNT(*) > 1 LIMIT 1000; -- Sample first
deduplication_log
sql CREATE TABLE deduplication_log ( id SERIAL PRIMARY KEY, table_name VARCHAR(255), deduplication_key VARCHAR(255), rows_removed INT, executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
sql -- Only deduplicate users who signed up in the last 30 days WITH ranked_users AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_login DESC) as rn FROM users WHERE signup_date > CURRENT_DATE - INTERVAL '30 days' ) DELETE FROM users WHERE id IN (SELECT id FROM ranked_users WHERE rn > 1);
-- After SELECT COUNT() as after_count FROM users; `` - Set up alerts for duplicate spikes: If yourHAVING COUNT() > 1` query suddenly returns 1000 rows instead of 10, something’s wrong with your ETL.
`` - Set up alerts for duplicate spikes: If your
signup_date
NULL
COALESCE
IS NOT DISTINCT FROM
ORDER BY
✅ DISTINCT or GROUP BY
"How do you keep the most recent row per group?"
✅ ROW_NUMBER() OVER (PARTITION BY key ORDER BY timestamp DESC)
ROW_NUMBER() OVER (PARTITION BY key ORDER BY timestamp DESC)
"What’s the difference between UNION and UNION ALL?"
RANK()
DENSE_RANK()
"You have a transactions table with duplicate transaction_ids. How do you remove duplicates while keeping the row with the highest amount?"- ✅ Answer: sql WITH ranked_transactions AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY transaction_id ORDER BY amount DESC) as rn FROM transactions ) DELETE FROM transactions WHERE transaction_id IN ( SELECT transaction_id FROM ranked_transactions WHERE rn > 1 );
transactions
transaction_id
amount
sql WITH ranked_transactions AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY transaction_id ORDER BY amount DESC) as rn FROM transactions ) DELETE FROM transactions WHERE transaction_id IN ( SELECT transaction_id FROM ranked_transactions WHERE rn > 1 );
Challenge:You have a products table with duplicate product_ids. Some rows have NULL in the price column. Write a query to: 1. Keep only one row per product_id.2. Prefer rows where price IS NOT NULL.3. If multiple rows have non-NULL price, keep the one with the highest price.
products
product_id
price
price IS NOT NULL
Solution:
WITH ranked_products AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY product_id ORDER BY CASE WHEN price IS NOT NULL THEN 0 ELSE 1 END, -- Prefer non-NULL price DESC -- Highest price first ) as rn FROM products ) SELECT product_id, product_name, price FROM ranked_products WHERE rn = 1;
Why it works:- CASE WHEN price IS NOT NULL THEN 0 ELSE 1 END ensures non-NULL rows are ranked first.- price DESC breaks ties by keeping the highest price.
CASE WHEN price IS NOT NULL THEN 0 ELSE 1 END
price DESC
SELECT key_column, COUNT(*) FROM table GROUP BY key_column HAVING COUNT(*) > 1;
ROW_NUMBER() OVER (PARTITION BY key ORDER BY timestamp ASC) as rn
ROW_NUMBER() OVER (PARTITION BY key ORDER BY timestamp DESC) as rn
DELETE FROM table WHERE id NOT IN (SELECT MIN(id) FROM table GROUP BY key);
CREATE TABLE deduped AS SELECT * FROM table GROUP BY key;
ROW_NUMBER() OVER (PARTITION BY key ORDER BY COALESCE(price, 0) DESC)
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.