Skip to content

MySQL - Handling Duplicates

This tutorial provides a comprehensive guide to managing duplicate records in MySQL. You'll learn why duplicates are problematic and explore modern techniques to prevent, identify, and remove them, both with SQL commands and through client-side application logic.

Duplicate data, or data redundancy, can cause significant issues in a database:

  • Data Integrity: It becomes unclear which record is the correct or most recent one, leading to inconsistent and unreliable reports.
  • Storage Inefficiency: Duplicates waste disk space, increasing storage costs and backup times.
  • Performance Degradation: Larger tables with redundant data can slow down query performance.
  • Application Errors: Software logic can break or produce incorrect results when it encounters unexpected duplicate entries.

The best way to handle duplicates is to prevent them from entering the database in the first place. This is achieved through database constraints.

A PRIMARY KEY constraint ensures that every row in a table is uniquely identifiable. It does not allow NULL values and the indexed columns must be unique. A table can have only one primary key.

CREATE TABLE users (
id INT AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
username VARCHAR(50) NOT NULL,
PRIMARY KEY(id),
UNIQUE KEY (email) -- Also a good idea to enforce unique emails
);

A UNIQUE constraint also ensures that all values in a column or a set of columns are unique. Unlike a PRIMARY KEY, you can have multiple UNIQUE constraints per table, and they can allow one NULL value.

CREATE TABLE products (
product_code VARCHAR(20) NOT NULL,
product_name VARCHAR(100) NOT NULL,
UNIQUE (product_code) -- Prevents duplicate product codes
);

When a constraint is in place, a standard INSERT will fail if it violates the uniqueness. MySQL provides ways to handle this gracefully:

  • INSERT IGNORE: If the new row is a duplicate, MySQL silently discards it without generating an error. The original row remains untouched.
  • REPLACE INTO: This command is a MySQL extension. If the row is new, it acts like INSERT. If it’s a duplicate, it first DELETES the existing row and then INSERTS the new one. (Caution: This changes the row’s internal ID and can cascade deletes).
  • INSERT … ON DUPLICATE KEY UPDATE (Recommended): This is the most flexible and widely used method. It attempts an INSERT, and if a duplicate key is found, it performs an UPDATE on the existing row instead. This avoids deletion and preserves the original row’s ID.
-- Using the modern, recommended approach
INSERT INTO products (product_code, product_name)
VALUES ('A-123', 'Super Widget')
ON DUPLICATE KEY UPDATE product_name = 'Super Widget v2';

Part 2: Finding and Identifying Duplicates

Section titled “Part 2: Finding and Identifying Duplicates”

If duplicates already exist in your table, you first need to find them.

This is the classic method to find which values are duplicated and how many times they appear.

-- Let's assume a contacts table with potential duplicates
SELECT
email, -- The column(s) that might be duplicated
COUNT(*) as duplicate_count
FROM contacts
GROUP BY email
HAVING COUNT(*) > 1;

This query will return all email addresses that appear more than once in the contacts table, along with their count.

Once you’ve identified duplicates, you need a strategy to remove them, typically keeping one version (e.g., the oldest or newest).

Method 1: Using a Temporary Table (Safe but requires downtime)

Section titled “Method 1: Using a Temporary Table (Safe but requires downtime)”

This method is safe and easy to understand. You create a new table with only the unique rows, then replace the original table.

-- Step 1: Create a temporary table with the unique rows you want to keep.
CREATE TABLE contacts_temp AS
SELECT * FROM contacts
GROUP BY email; -- Or use MIN(id) to keep the first entry
-- Step 2: Drop the original table.
DROP TABLE contacts;
-- Step 3: Rename the temporary table to the original name.
RENAME TABLE contacts_temp TO contacts;

Method 2: Using a Self-Join or Subquery (Advanced)

Section titled “Method 2: Using a Self-Join or Subquery (Advanced)”

This method deletes duplicates in place. It’s more complex but avoids table replacement. It deletes all rows that have a lower id than another row with the same email.

DELETE t1 FROM contacts t1
INNER JOIN contacts t2
WHERE
t1.id < t2.id AND
t1.email = t2.email;

Method 3: Using Window Functions (Modern & Powerful)

Section titled “Method 3: Using Window Functions (Modern & Powerful)”

Using a Common Table Expression (CTE) with the ROW_NUMBER() window function is a clean and powerful way to handle this, available in MySQL 8.0+.

WITH NumberedContacts AS (
SELECT
id,
ROW_NUMBER() OVER(PARTITION BY email ORDER BY id ASC) as row_num
FROM contacts
)
DELETE FROM contacts
WHERE id IN (SELECT id FROM NumberedContacts WHERE row_num > 1);

This query partitions the data by email, numbers the rows within each partition, and then deletes any row whose number is greater than 1, effectively keeping only the first entry for each email.

Your application code should be prepared to handle potential duplicate entry errors when a database constraint is violated.

import mysql.connector
import os
def add_user(email, username):
connection = None
try:
connection = mysql.connector.connect(
host=os.getenv('DB_HOST', 'localhost'),
user=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', 'password'),
database='your_database'
)
cursor = connection.cursor()
insert_query = "INSERT INTO users (email, username) VALUES (%s, %s)"
cursor.execute(insert_query, (email, username))
connection.commit()
print(f"User '{username}' with email '{email}' added successfully.")
except mysql.connector.Error as err:
# MySQL error code for duplicate entry is 1062
if err.errno == 1062:
print(f"Error: A user with the email '{email}' already exists.")
else:
print(f"Database Error: {err}")
if connection:
connection.rollback()
finally:
if connection and connection.is_connected():
cursor.close()
connection.close()
# --- Test Cases ---
add_user('test@example.com', 'testuser1') # Should succeed
add_user('test@example.com', 'testuser2') # Should fail gracefully