Home › Database › MySQL 1062 Duplicate Entry — Upsert or Ignore
Beginner 5 min · September 23, 2026

MySQL 1062 Duplicate Entry — Upsert or Ignore

Fix MySQL error 1062 by reading the key name in the message, then choosing INSERT IGNORE or ON DUPLICATE KEY UPDATE.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Drawn from code that ran under real load.

Follow
✓ Production
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 11 min
  • ✓A MySQL database with a UNIQUE key you can inspect
  • ✓Comfort running GROUP BY queries to find duplicates
  • ✓A write path (API, worker, or loader) that hits the key
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Error 1062 means a UNIQUE constraint (or primary key) rejected your row: the value already exists, and the key name in the message tells you exactly which constraint fired
  • Read it literally: Duplicate entry 'a@x.com' for key 'users.email' names the value and the unique key — no guessing needed before you query
  • Pick one of three writes: plain INSERT (fail loud), INSERT IGNORE (skip silently), or INSERT ... ON DUPLICATE KEY UPDATE (merge) — each suits a different retry story
  • Make retried writes idempotent with unique keys plus upserts, so a replayed webhook or timed-out commit can't create a second row
✦ Definition~90s read
What is MySQL 1062 Duplicate Entry Fix?

Error 1062 is a uniqueness violation: your INSERT or UPDATE would create two rows with the same value in a column (or column set) guarded by a PRIMARY KEY or UNIQUE index, so InnoDB refuses. The message hands you both halves of the diagnosis — the duplicate value ('a@x.com') and the constraint name ('users.email', or 'PRIMARY' for the primary key).

★
Picture a theater where every seat is reserved by email — one seat per address.

There's no deeper mystery to decode; the work is deciding what the duplicate means.

Three stories produce it. Genuine duplicates: the data really collides (two signups, one email) and the application must handle it as a business case. Replays: the first write succeeded but its acknowledgment was lost — timeout, crash, webhook redelivery — so the retry re-inserts the same logical row.

Races: two concurrent requests both checked 'no such row' and both inserted, because check-then-insert isn't atomic. Replays and races are the production pair: they mean the write path isn't idempotent, and the 1062 is actually your constraint doing its job.

The fixes map to the stories. INSERT IGNORE skips conflicting rows silently — right for bulk best-effort loads, dangerous as a blanket habit since it also skips other errors. ON DUPLICATE KEY UPDATE merges: insert fresh rows, update specified columns on conflict — the workhorse for sync jobs and retried writes.

And the structural fix is unique keys as arbiters plus atomic single-statement writes, so concurrent duplicates resolve inside InnoDB instead of in your application logs.

Plain-English First

Picture a theater where every seat is reserved by email — one seat per address. Error 1062 is the usher stopping you: this email already has a seat. Maybe you genuinely double-booked (a real duplicate), or your friend re-sent the request because the first reply got lost (a retry), or two box offices sold the same seat at once (a race). Each story has its fix: IGNORE the double, merge into the seat, or write atomically.

ERROR 1062 (23000): Duplicate entry 'a@x.com' for key 'users.email'. It's the most honest error in MySQL — it names the value and the constraint — yet teams still lose hours to it. Sometimes it's a genuine data problem: two real users, one email. Sometimes it's infrastructure: a retried webhook, a timed-out commit the app replayed, or two requests racing through a check-then-insert gap.

The cost of mishandling it is real. Ignore every 1062 and you silently drop legitimate writes. Retry blindly and you amplify storms. Remove the unique key to 'stop the errors' and you trade loud rejections for silent duplicate rows that corrupt reports for months.

This guide shows the decision tree: reading the key name, reproducing the duplicate, choosing between INSERT, INSERT IGNORE, and ON DUPLICATE KEY UPDATE, closing check-then-insert races, and designing idempotent writes so retries become safe by construction. The error never lies about what collided — only your write path knows why it arrived twice, and that's the part this guide teaches you to read.

Read the Key Name in the Message

Duplicate entry 'a@x.com' for key 'users.email' contains the full diagnosis: the colliding value and the constraint that rejected it. Key names map straight to schema: 'PRIMARY' means the primary key (check whether it's an app-supplied reference rather than auto-increment), 'email' or 'users.email' names the unique key and, by convention, its column. Multi-column keys report the key name, not the columns — SHOW CREATE TABLE translates the name into the column list.

Confirm with two queries before acting. SHOW CREATE TABLE shows every key, its columns, and whether the supposedly-unique business rule is actually enforced. SHOW INDEX FROM adds cardinality and uniqueness flags per key. Together they answer the question the message can't: is the constraint right and the data wrong, or is the data right and the constraint wrong? A unique key on a column that legitimately repeats (a nullable 'external_id' backfilled with zeros) is a schema bug wearing an error costume.

Log the message verbatim in tickets. The value-plus-key pair lets the next engineer reproduce in one SELECT instead of re-deriving the collision from app logs — and trend analysis on key names shows whether 1062s cluster on one constraint (a path bug) or scatter (a data-quality drift).

read_key_1062.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Translate the key name into columns + uniqueness
SHOW CREATE TABLE users;
SHOW INDEX FROM users;

-- The row that already won (compare with the failing write)
SELECT * FROM users WHERE email = 'a@x.com';

-- Is the constraint sane? (NULLs, zeros, and placeholders)
SELECT email, COUNT(*) FROM users
GROUP BY email HAVING COUNT(*) > 1 LIMIT 20;
📊 Production Insight
A 1062 on key 'external_id' traced to a backfill that wrote zeros into 40k rows — the constraint was right, the backfill was wrong. The key name named the fix in one line.
🎯 Key Takeaway
The message names value plus constraint — translate with SHOW CREATE TABLE, then judge whether data or schema is wrong.

Reproduce the Duplicate Before Choosing the Fix

Never pick IGNORE versus upsert from the error alone — reproduce the collision and classify it. The GROUP BY ... HAVING COUNT(*) > 1 query surfaces every violated value at once: one row with yesterday's timestamp is a genuine business collision; thousands of rows in the last hour is a replay storm or a race. Volume and timing pick the fix, not philosophy.

Check for placeholder collisions specifically. Backfills that wrote '' or 0 into newly-constrained columns manufacture thousands of 'duplicates' that aren't real dupes — they're missing data wearing a shared mask. The remedy there is cleaning the placeholders (NULL them, then add the key), not upserting around them. Similarly, case-only collisions ('A@x.com' vs 'a@x.com') under a case-insensitive collation need a normalization rule, not a bigger hammer.

Write the reproduction as a runnable query in the ticket, with the violating values attached. Fixes for 1062 get reverted more than most ('why did signups drop?'), and a saved repro lets the next engineer verify the fix against the exact collision instead of re-discovering it from graphs. Include first_seen and last_seen per value — a burst within one hour means replay or race, while scattered months-old dupes mean data drift with a different owner.

repro_dupe.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
-- Every violated value at once (volume + timing classify it)
SELECT email, COUNT(*) AS n,
  MIN(created_at) AS first_seen, MAX(created_at) AS last_seen
FROM users
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY n DESC
LIMIT 20;

-- Placeholder collisions? (backfill zeros/empties sharing one mask)
SELECT email, COUNT(*) FROM users
WHERE email IN ('', 'unknown', '0')
GROUP BY email;
📊 Production Insight
A burst of 9,000 identical-timestamp dupes proved replay storm in one query — no code reading required. Timing classifies faster than stack traces.
🎯 Key Takeaway
Reproduce with GROUP BY/HAVING first: timing and volume tell genuine collision, replay, and race apart.

Three Writes: INSERT vs IGNORE vs ON DUPLICATE KEY UPDATE

Plain INSERT fails loud on conflict — the right default for paths where a duplicate is genuinely unexpected, because the 1062 becomes your alert. INSERT IGNORE downgrades conflicts (and several other errors) to warnings and skips the row: right for best-effort bulk loads where some rows may already exist, dangerous as a habit since it also swallows NOT NULL and truncation complaints you'd want to hear. Know that IGNORE skips the row entirely — no update, no counter, just a warning count.

INSERT ... ON DUPLICATE KEY UPDATE is the merge workhorse: new values insert, conflicting rows update the columns you name. Use VALUES(col) (or the 8.0.19+ alias syntax) to reference the would-be-inserted value, and keep the update list explicit — updating every column including created_at destroys audit history on every replay. For sync jobs and webhook consumers, upsert is almost always the answer: first delivery inserts, redelivery refreshes, nothing ever errors.

Avoid REPLACE unless you mean delete-then-insert. It removes the conflicting row and inserts fresh, which burns auto-increment values, fires DELETE triggers, and breaks foreign keys that the upsert would have preserved. REPLACE in a retry path is a data-loss footgun with a convenient syntax.

three_writes_1062.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- Default: fail loud (duplicates should page you here)
INSERT INTO users (email, name) VALUES ('a@x.com', 'Ann');

-- Best-effort bulk: skip conflicts silently (check warnings after)
INSERT IGNORE INTO users (email, name) VALUES ('a@x.com', 'Ann');
SHOW WARNINGS;

-- Merge workhorse: insert fresh, refresh on conflict
INSERT INTO users (email, name) VALUES ('a@x.com', 'Ann Lee')
ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW();

-- Avoid: REPLACE deletes the old row first (triggers, FKs, auto-inc burn)
⚠ REPLACE Is Delete-Then-Insert
REPLACE removes the conflicting row before inserting — firing DELETE triggers, breaking foreign keys, and burning auto-increment values. In retry paths that means redelivery can destroy data. Prefer ON DUPLICATE KEY UPDATE.
📊 Production Insight
Switching a webhook consumer from INSERT to ON DUPLICATE KEY UPDATE cleared a 9,000-job backlog in 11 minutes with zero double-charges — replays became merges.
🎯 Key Takeaway
Default to loud INSERT, use IGNORE for best-effort bulk, upsert for sync/retry paths — and never REPLACE casually.

Races: Check-Then-Insert Always Loses

The pattern looks safe: SELECT to check existence, INSERT if absent. Under concurrency it's a guaranteed race — two requests both see 'absent' and both insert, and the loser's 1062 is the constraint saving you from a duplicate row. The error rate scales with traffic overlap, which is why the path 'works in staging, 1062s in prod': staging never runs the two requests in the same millisecond.

The fix is making the database the arbiter with a single atomic statement. INSERT ... ON DUPLICATE KEY UPDATE collapses check and write into one InnoDB operation: concurrent duplicates serialize on the unique index, one wins the insert, the other becomes an update. No application lock, no distributed mutex, no timing window. The unique key isn't just a guardrail here — it's the concurrency control.

For multi-step creations (user + profile + preferences), wrap the upsert plus dependent writes in one transaction, keyed on the same idempotency value. And never 'fix' races by removing the unique key: without the arbiter, both inserts succeed and you trade loud 1062s for silent duplicate rows that poison reports for quarters. When dependent rows must follow (profiles, preferences), wrap the upsert and the follow-ups in one transaction keyed on the same idempotency value — partial creations are just duplicates with extra steps.

atomic_upsert_1062.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Race-prone (two requests can both see 'absent'): avoid
-- SELECT id FROM users WHERE email = 'a@x.com';
-- INSERT INTO users (email) VALUES ('a@x.com');

-- Atomic: InnoDB arbitrates via the unique index
INSERT INTO users (email, name) VALUES ('a@x.com', 'Ann')
ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW();

-- Multi-step creation, one idempotency value, one transaction
START TRANSACTION;
INSERT INTO users (email) VALUES ('a@x.com')
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);
INSERT INTO profiles (user_id) VALUES (LAST_INSERT_ID())
ON DUPLICATE KEY UPDATE user_id = user_id;
COMMIT;
📊 Production Insight
A signup path that 'checked first' 1062'd exactly during campaign traffic — never in staging. One atomic upsert ended the race without any locking service.
🎯 Key Takeaway
Collapse check-then-insert into one atomic upsert and let the unique index arbitrate concurrency.

Idempotency: Make Retries Safe by Construction

Retries are a fact of distributed life: timeouts lose acknowledgments, providers redeliver, deploys restart workers mid-batch. A write path is idempotent when replaying it N times has the effect of doing it once — and the recipe is a unique key on the natural deduplication value (provider reference, idempotency token, request id) plus upsert-or-ignore semantics on every insert. With that pair, redelivery is a no-op by construction instead of an incident.

Wire the token through the whole path: the client generates it, the API stores it in a UNIQUE column alongside the payload, and the worker upserts on it. Test it the way production will exercise it — fire the same webhook twice in CI and assert a single row with the merged state. The replay test below does exactly that against staging in ten seconds.

Route repeated key violations to a dead-letter queue with an alert, not back into the retry loop. A 1062 on a path you believe idempotent means the merge logic is wrong (updating columns it shouldn't) or two different business events share a token — both need human eyes, not a 31st retry. Log the token with every attempt so support can trace a double-charge scare to its two deliveries in seconds.

replay_test_1062.shBASH
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# Fire the same webhook twice: exactly one payment row must exist
set -euo pipefail
TOKEN="idem-$(date +%s)"
curl -s -X POST https://staging.example.com/hooks/pay \
  -H "Idempotency-Key: $TOKEN" -d '{"ref":"PAY-881","amount":5000}' > /dev/null
curl -s -X POST https://staging.example.com/hooks/pay \
  -H "Idempotency-Key: $TOKEN" -d '{"ref":"PAY-881","amount":5000}' > /dev/null
mysql -u app -h db-staging -N -e \
  "SELECT COUNT(*) FROM payments WHERE provider_ref='PAY-881';"
# expect: 1 (replay merged, not duplicated)
💡Test With Doubled Delivery
Every webhook consumer gets a CI test that delivers the same payload twice and asserts one row. Providers will redeliver eventually — the test makes it a non-event instead of a 3 AM discovery.
📊 Production Insight
A doubled-delivery CI test now guards every consumer — it caught a non-idempotent refactor three weeks before the provider's next redelivery wave.
🎯 Key Takeaway
Unique token plus upsert semantics makes replay a no-op; dead-letter repeated violations instead of re-retrying them.

Cleanup: Removing Dupes Before Adding the Constraint

Sometimes the 1062 arrives while creating the unique key itself: ALTER TABLE ... ADD UNIQUE fails because duplicates already exist. Don't force it — reconcile first. The self-join DELETE below keeps the earliest row per value and removes the rest; run it as SELECT first to eyeball what would die, then as DELETE inside a transaction with a snapshot taken minutes before. On large tables, batch the delete to avoid a megatransaction that stalls replication.

Decide keep-rules per case, not by default: earliest row, most recently updated, or the row with the richest payload. Log the doomed ids to an archive table before deleting — 'we removed 40k backfill zeros' needs evidence when finance asks about the row count next quarter. After cleanup, add the constraint and immediately rerun the GROUP BY/HAVING check to prove zero violations.

Backfill hygiene prevents the sequel. New UNIQUE columns should default to NULL (NULLs don't collide in MySQL unique indexes) rather than '' or 0, and backfills should write real values or NULL — never a shared placeholder that manufactures 40,000 instant duplicates. Test the new constraint in staging with production-shaped data first — a key that builds on 10k staging rows can still fail on 50M production rows with edge-case dupes.

dedupe_add_key.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Preview what would die (run first, read carefully)
SELECT email, COUNT(*) AS n FROM users
GROUP BY email HAVING COUNT(*) > 1;

-- Archive, then keep earliest row per value
CREATE TABLE users_dedupe_archive AS
SELECT u.* FROM users u
JOIN (SELECT email, MIN(id) AS keep_id FROM users GROUP BY email HAVING COUNT(*) > 1) d
USING (email) WHERE u.id <> d.keep_id;

DELETE u FROM users u
JOIN (SELECT email, MIN(id) AS keep_id FROM users GROUP BY email HAVING COUNT(*) > 1) d
USING (email) WHERE u.id <> d.keep_id;

ALTER TABLE users ADD UNIQUE KEY uniq_email (email);
📊 Production Insight
A zeros-backfill had manufactured 40k 'duplicates' that blocked the new key for a week. Archive-then-delete plus NULL defaults ended the saga in one maintenance window.
🎯 Key Takeaway
Reconcile before constraining: archive dupes, keep by rule, add the key, and default new unique columns to NULL.
● Production incidentPOST-MORTEMseverity: high

A Retried Webhook Turned 1062s Into a 40-Minute Checkout Stall

Symptom
At 3:12 PM a payment provider's network blip triggered mass webhook redelivery: 12,000 duplicate payment notifications in 25 minutes. Every replay ran a plain INSERT into payments, hit 1062 on the provider-reference unique key, threw, and landed in the retry queue — which retried on a fixed 30-second loop with no backoff. By 3:30 PM the retry queue held 9,000 doomed jobs hammering the same constraint, connection slots filled with failing transactions, and genuine new payments started timing out behind the retry storm.
Assumption
The team assumed a provider outage had corrupted payloads, so they paused the webhook consumer to 'stop the bad data' — which only grew the backlog. When they resumed, the flood restarted identically. Thirty minutes went to diffing payloads looking for corruption that never existed; every payload was valid, just delivered twice.
Root cause
Two gaps compounded. First, the insert path wasn't idempotent: plain INSERT plus application-level 'check if exists first' — a race-friendly, replay-hostile pattern. Second, the retry policy retried constraint violations (1062) identically to transient failures, with no backoff and no dead-lettering. The unique key worked exactly as designed; everything around it conspired to re-offend 12,000 times.
Fix
They drained the queue by switching the consumer to INSERT ... ON DUPLICATE KEY UPDATE payments SET status=VALUES(status), updated_at=NOW() — replays became harmless merges in one deploy. A jittered exponential backoff plus dead-letter routing for repeated key violations joined the worker config. The 9,000-job backlog cleared in 11 minutes with zero double-charges, and a replay-the-same-webhook-twice test joined CI the same day.
Key lesson
  • Make every webhook and retry path idempotent by construction: unique keys plus upsert semantics turn redelivery from an incident into a no-op.
  • Never retry constraint violations like transient errors: 1062 means the write resolved already — back off, dead-letter, and alert instead of hammering.
  • Reproduce with doubled delivery in CI: firing each webhook twice catches non-idempotent inserts months before a provider blip does.
Production debug guideFive steps from the message to the story — genuine duplicate, replay, or race.5 entries
Symptom · 01
Error 1062 names a value and key: Duplicate entry 'X' for key 'Y'
→
Fix
Read it literally, then confirm the constraint: SHOW CREATE TABLE payments; — find the UNIQUE KEY or PRIMARY KEY named Y and its columns. If Y is 'PRIMARY', the colliding column is the primary key (often an app-supplied id or reference, not auto-increment). This single statement tells you which business rule fired before you touch app code.
Symptom · 02
You need to see the existing row that won
→
Fix
Query the duplicate directly: SELECT * FROM users WHERE email = 'a@x.com'; Compare created_at, source, and payload against the failing write's logs. A row created seconds before the failure with identical payload screams replay; a row from months ago with different data screams genuine collision; two rows milliseconds apart scream race.
Symptom · 03
Duplicates appear in bursts matching deploys, restarts, or provider incidents
→
Fix
Correlate timestamps: SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:%i'), COUNT(*) FROM payments WHERE created_at > NOW() - INTERVAL 2 HOUR GROUP BY 1; Spikes aligned with a timeout change, a deploy, or a provider status page mean replays — the first write committed, the ack got lost, and the retry re-inserted. Fix the write path, not the data.
Symptom · 04
Two concurrent requests both passed an existence check, then one 1062d
→
Fix
Search the code for SELECT-then-INSERT on the constrained columns — that gap is the race. Prove it with SHOW ENGINE INNODB STATUS or the query log showing interleaved check/insert pairs. Replace the pair with a single atomic INSERT ... ON DUPLICATE KEY UPDATE and let InnoDB arbitrate: the loser's statement becomes an update instead of an error.
Symptom · 05
Bulk loads fail halfway with 1062 and leave partial data
→
Fix
Stage first, then merge: LOAD DATA or batch INSERTs into a staging table with no unique key, dedupe with SELECT email, COUNT() FROM stage GROUP BY email HAVING COUNT() > 1; reconcile against production rows, and only then INSERT ... ON DUPLICATE KEY UPDATE into the live table. Partial-failure loads need set-based reconciliation, not row-by-row ignoring.
MySQL 1062 Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Genuine business collisionExisting row is old, different payloadHandle as product case (merge/surface)Validate uniqueness in the form first
Replayed write (timeout/redelivery)Bursts aligned with incidents; identical payloadON DUPLICATE KEY UPDATEIdempotency tokens + doubled-delivery CI
Check-then-insert raceInterleaved check/insert pairs; staging-cleanAtomic single-statement upsertNever SELECT-then-INSERT on hot paths
Bulk load with internal dupesStaging GROUP BY/HAVING shows repeatsStage, dedupe, then mergeDedupe stage before touching live tables
Placeholder backfill collisionThousands share ''/0 in new columnArchive, NULL them, then add keyDefault new unique columns to NULL
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
read_key_1062.sqlSHOW CREATE TABLE users;Read the Key Name in the Message
repro_dupe.sqlSELECT email, COUNT(*) AS n,Reproduce the Duplicate Before Choosing the Fix
three_writes_1062.sqlINSERT INTO users (email, name) VALUES ('a@x.com', 'Ann');Three Writes
atomic_upsert_1062.sqlINSERT INTO users (email, name) VALUES ('a@x.com', 'Ann')Races
replay_test_1062.shset -euo pipefailIdempotency
dedupe_add_key.sqlSELECT email, COUNT(*) AS n FROM usersCleanup

Key takeaways

1
Error 1062 names value plus constraint
translate with SHOW CREATE TABLE first.
2
Classify the story by timing and volume
genuine collision, replay, or race.
3
Loud INSERT by default; IGNORE for bulk best-effort; upsert for sync and retries.
4
Atomic single-statement upserts let the unique index arbitrate concurrency.
5
Idempotency tokens plus upsert semantics make redelivery a no-op.
6
Reconcile before constraining, and never delete the key to silence the error.

Common mistakes to avoid

5 patterns
×

SELECT-then-INSERT on concurrent hot paths

Symptom
Clean in staging, 1062s under real overlap — the gap always loses eventually.
Fix
Use atomic INSERT ... ON DUPLICATE KEY UPDATE and let the unique index arbitrate.
×

Catching 1062 as normal control flow without idempotency

Symptom
Retries amplify storms; half-merged rows accumulate across attempts.
Fix
Make the write idempotent (token + upsert) so the catch path converges instead of looping.
×

Reaching for REPLACE to silence conflicts

Symptom
Conflicts vanish but triggers fire, FKs break, and auto-increment burns.
Fix
Prefer ON DUPLICATE KEY UPDATE with an explicit column list; reserve REPLACE for scratch tables.
×

Blanket INSERT IGNORE across the codebase

Symptom
Real errors (NOT NULL, truncation) vanish into warnings nobody reads.
Fix
Scope IGNORE to best-effort bulk loads and check SHOW WARNINGS; default to loud INSERT elsewhere.
×

Deleting the unique key to stop the errors

Symptom
1062s end; silent duplicate rows corrupt reports for months.
Fix
Keep the constraint — it's the arbiter. Fix the write path (upsert, tokens, races) instead.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does MySQL error 1062 tell you?
Q02SENIOR
When do you use INSERT IGNORE versus ON DUPLICATE KEY UPDATE?
Q03SENIOR
Two requests race through check-then-insert and one gets 1062. Fix it.
Q04SENIOR
ADD UNIQUE fails on existing duplicates in a 50M-row table. Walk through...
Q05SENIOR
Design a webhook consumer that survives provider redelivery.
Q01 of 05JUNIOR

What does MySQL error 1062 tell you?

ANSWER
A UNIQUE or PRIMARY KEY constraint rejected the write: the value already exists. The message names both the duplicate value and the key, so diagnosis starts with one SELECT for the winning row.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I use INSERT IGNORE or ON DUPLICATE KEY UPDATE?
02
Does INSERT IGNORE hide real errors?
03
Why did my auto-increment jump after failed inserts?
04
How do I read a 1062 on a composite unique key?
05
When is REPLACE actually appropriate?
06
Can I make LOAD DATA idempotent?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Drawn from code that ran under real load.

Follow
✓ Verified
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
🔥

That's MySQL. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
MySQL Too Many Connections Fix
5 / 7 · MySQL
Next
Postgres Relation Does Not Exist Fix
→