Skip to main content

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples in this guide work on VillageSQL. Install Now →
MySQL doesn’t have an UPSERT keyword, but it has three ways to insert a row or handle a conflict: INSERT ... ON DUPLICATE KEY UPDATE, REPLACE INTO, and INSERT IGNORE. They behave very differently — picking the wrong one can silently corrupt data.

INSERT … ON DUPLICATE KEY UPDATE

When the insert would violate a unique constraint (primary key or unique index), MySQL runs the UPDATE instead:
To reference the value from the attempted insert (rather than the existing row), use row aliases:
The older VALUES(col) syntax also works but is deprecated — use row aliases instead:

REPLACE INTO

REPLACE deletes the conflicting row and inserts a new one. It looks like an upsert but behaves like a delete + insert:
This fires BEFORE DELETE and AFTER DELETE triggers on the target table and resets the AUTO_INCREMENT counter for the new row. Because the delete is a real row delete, ON DELETE CASCADE and the other foreign key actions on referencing tables DO fire. Any columns not listed in the REPLACE get their default values — not the previous row’s values. Use REPLACE only when you genuinely want to discard the old row entirely.

INSERT IGNORE

INSERT IGNORE downgrades errors to warnings. On a key conflict it skips the row; on a type mismatch or truncation it writes a coerced value instead:
The problem with INSERT IGNORE is that it suppresses all errors, not just duplicates — and the non-duplicate cases are worse than a skipped row. INSERT IGNORE INTO page_views VALUES ('/new', 'not_a_number') inserts ('/new', 0) with only a warning: a silent, corrupted write rather than a no-op. Use it only when you genuinely don’t care about the outcome.

Comparison

AUTO_INCREMENT Side Effect

ON DUPLICATE KEY UPDATE increments the AUTO_INCREMENT counter even when it takes the update path, not the insert path. Over time this creates gaps in the sequence:
This is expected behavior. If your application depends on a gap-free sequence, use a separate counter table or handle sequence generation in the application.

Frequently Asked Questions

Is ON DUPLICATE KEY UPDATE atomic?

Yes — the insert attempt and the update happen in a single atomic operation. You don’t need to wrap it in a transaction to avoid a race condition between checking existence and inserting.

What if multiple unique indexes conflict?

If an insert violates more than one unique constraint simultaneously, MySQL updates only one row — which one is undefined. Avoid designs where a single INSERT can conflict on multiple unique indexes. If your schema has overlapping unique constraints, test the behavior carefully.

Can I use ON DUPLICATE KEY UPDATE with multi-row inserts?

Yes:
Each conflicting row triggers its own update. With row aliases, use VALUES for the multi-row case and AS new for the alias.

Troubleshooting

See also