How to Find and Delete Duplicate Rows in SQL
Identify rows with duplicate column values using GROUP BY and HAVING count(*) > 1, and safely remove duplicates using ROW_NUMBER() CTEs.
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;Step-by-Step Query Breakdown
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;Groups rows by the email column and filters out groups with only 1 occurrence, showing which values are duplicated.
SELECT *
FROM users
WHERE email IN (
SELECT email
FROM users
GROUP BY email
HAVING COUNT(*) > 1
)
ORDER BY email, created_at;Subquery returns all full row records belonging to duplicated groups for manual comparison.
WITH ranked_duplicates AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id ASC) as row_num
FROM users
)
DELETE FROM users
WHERE id IN (
SELECT id FROM ranked_duplicates WHERE row_num > 1
);Assigns a row rank per partition. Keeps row_num = 1 and permanently deletes subsequent duplicates.
Critical SQL Pitfalls & Precautions
- β’Always test your DELETE query with a SELECT first inside a BEGIN TRANSACTION / ROLLBACK block.
- β’If your table lacks a unique primary key (ID), use the database physical row identifier (ctid in PostgreSQL, rowid in SQLite).
Find & Delete Duplicate Rows - Frequently Asked Questions
Common questions about transactional safety, indexing, and engine compatibility.
Add a UNIQUE constraint or unique index: "ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);".
Related SQL Query Guides
Browse All SQL RecipesCompare row-by-row logging (DELETE), fast table emptying with auto-increment reset (TRUNCATE), and complete table removal (DROP).
Atomically insert a new row or update an existing row if a unique constraint or primary key conflict is encountered.
Compute cumulative running totals over time or partitioned groups using SUM() OVER (ORDER BY ...).
Build a production database index on a table with millions of rows without locking out live INSERT, UPDATE, or DELETE operations.
Update values in one table based on matching columns or aggregations in a secondary table.
Reset the next auto-generated primary key ID back to 1 or synchronize it with the maximum existing ID in the table.