When This Error Hits
You're running an INSERT query on a MySQL 5.7 or 8.0 table — maybe in production, maybe in a staging environment — and suddenly you get: ERROR 1062 (23000): Duplicate entry 'john_doe' for key 'users.username'. Common triggers: a cron job importing CSV data twice, a user registration form that submitted two simultaneous requests, or a migration script that ran the same batch twice.
What's Actually Happening
MySQL enforces uniqueness at the storage engine level. When you define a column with UNIQUE constraint (or a composite unique index), InnoDB builds a hidden index tree looking for collisions before every INSERT. If the new row's value already exists in that index, MySQL aborts the whole statement and rolls back. It's not a warning — it's a hard stop.
The root cause is always the same: you're trying to insert a value that already exists in a column or set of columns marked as unique. But the interesting part is why it exists. Could be orphan data from a partial import, could be a race condition between two concurrent requests, or maybe your application logic didn't check for duplicates first.
The Fix: Three Approaches, Pick the Right One
Step 1: Identify the Conflicting Key
First, find the exact row causing trouble. Run:
SHOW INDEX FROM users WHERE Key_name = 'users.username';
SELECT * FROM users WHERE username = 'john_doe';This tells you the structure of the unique index and whether the value actually exists. If you're dealing with a composite unique key (e.g., UNIQUE (email, tenant_id)), you need to match all columns in the constraint.
Step 2: Choose Your Weapon
Don't blindly delete the existing row — you'll lose data. Instead, handle the conflict gracefully:
- If you want to update existing rows (like updating a timestamp or incrementing a counter): Use
INSERT ... ON DUPLICATE KEY UPDATE. This tells MySQL: "Try to insert; if a duplicate key collision happens, run this UPDATE instead." Example:
INSERT INTO users (username, email, login_count)
VALUES ('john_doe', 'john@example.com', 1)
ON DUPLICATE KEY UPDATE login_count = login_count + 1, updated_at = NOW();- If you want to replace the existing row entirely: Use
REPLACE INTO. This is destructive — it deletes the old row and inserts a new one. Watch out for auto-increment IDs shifting and foreign key cascade deletes. Example:
REPLACE INTO users (username, email, created_at)
VALUES ('john_doe', 'john@example.com', NOW());- If you want to skip duplicates silently: Use
INSERT IGNORE. This suppresses the error and skips rows that would cause a violation. Be careful — you won't know anything was skipped unless you checkROW_COUNT()after the statement.
Step 3: Test on a Staging Clone First
The reason step 3 matters: REPLACE INTO and INSERT IGNORE have side effects you might not expect. REPLACE INTO increments the auto-increment counter even if the row already existed, which can cause gaps in your IDs. On high-traffic tables, this can lead to integer overflow on a primary key column if you're not watching. INSERT IGNORE hides other errors like foreign key violations — you'll think the data was inserted when it wasn't.
When the Fix Still Fails
If you apply one of these approaches and still see error 1062, check these three things:
- Is the unique index composite? A duplicate on a multi-column unique key requires all indexed columns to match. Your WHERE clause in the ON DUPLICATE KEY UPDATE part must cover all columns in the index, or MySQL will still throw the error.
- Is there a trigger or generated column interfering? MySQL triggers run after the duplicate check but before the row is actually inserted. If a trigger modifies a column that's part of a unique constraint, you can create a collision at the last moment. Check with
SHOW TRIGGERS. - Is the table in strict mode? MySQL's strict SQL mode can cause insert failures that look like duplicate key errors. Run
SELECT @@sql_modeand check forSTRICT_TRANS_TABLES. If you're seeing error 1062 alongside error 1366 (incorrect integer value), the fix is different — you're dealing with data type mismatch, not a real duplicate.
In my experience, 80% of "impossible" duplicate errors come from either a composite index that the developer forgot about, or a trigger modifying data. Check both before diving deeper.