SQL Upsert: INSERT ON DUPLICATE KEY UPDATE and REPLACE
An upsert means “insert this row, or update it if it already exists”. Syncing a product feed, counting page views and saving user settings all need it. MySQL offers three tools, INSERT ... ON DUPLICATE KEY UPDATE, INSERT IGNORE and REPLACE, and they behave very differently when a row already exists.
- Upsert with
INSERT ... ON DUPLICATE KEY UPDATE - Refer to the new values with a row alias
- Skip duplicates with
INSERT IGNORE - Why
REPLACEdeletes before it inserts
What counts as a duplicate?
All three tools react to a clash on a PRIMARY KEY or UNIQUE index. Without such a key there is no “existing row” to detect, and every insert simply adds a row.
| product_id | product_name | category | price | stock |
|---|---|---|---|---|
| 1 | Wireless Mouse | Electronics | 799 | 120 |
| 2 | Mechanical Keyboard | Electronics | 3499 | 45 |
| 3 | USB-C Hub | Electronics | 1999 | 0 |
| 4 | Office Chair | Furniture | 8999 | 12 |
| 5 | Standing Desk | Furniture | 18999 | 5 |
| 6 | Notebook Pack | Stationery | 249 | 300 |
| 7 | Gel Pens | Stationery | 199 | 250 |
| 8 | Desk Lamp | Furniture | 1499 | 30 |
| 9 | Monitor Arm | Furniture | 2799 | 18 |
INSERT … ON DUPLICATE KEY UPDATE
A supplier sends a price list. Product 2 exists (update it), product 10 is new (insert it):
INSERT INTO products (product_id, product_name, category, price, stock)VALUES (2, 'Mechanical Keyboard', 'Electronics', 3299, 60), (10, 'Webcam HD', 'Electronics', 2499, 40) AS newON DUPLICATE KEY UPDATE price = new.price, stock = new.stock; SELECT product_id, product_name, price, stock FROM products WHERE product_id IN (2, 10);| product_id | product_name | price | stock |
|---|---|---|---|
| 2 | Mechanical Keyboard | 3299 | 60 |
| 10 | Webcam HD | 2499 | 40 |
- MySQL tries to insert each row.
- Product 2 clashes with the existing primary key, so instead of failing, MySQL runs the
UPDATEpart on that existing row. AS newis a row alias:new.pricemeans “the price from the row I tried to insert”.- Product 10 has no clash, so it is inserted normally.
- MySQL reports 3 rows affected: it counts 1 for each inserted row and 2 for each updated row.
Older code writes price = VALUES(price). That form still works but is deprecated since MySQL 8.0.20; use the row alias.
Counters with ON DUPLICATE KEY
A classic use: count events per day without checking first whether the row exists:
CREATE TABLE page_views (page VARCHAR(40) PRIMARY KEY, views INT NOT NULL);INSERT INTO page_views VALUES ('sql-tutorial', 1) ON DUPLICATE KEY UPDATE views = views + 1;INSERT INTO page_views VALUES ('sql-tutorial', 1) ON DUPLICATE KEY UPDATE views = views + 1;INSERT INTO page_views VALUES ('sql-joins', 1) ON DUPLICATE KEY UPDATE views = views + 1;SELECT * FROM page_views;| page | views |
|---|---|
| sql-joins | 1 |
| sql-tutorial | 2 |
- The first insert creates
sql-tutorialwith 1 view. - The second clashes, so
views = views + 1makes it 2. sql-joinsis new and starts at 1.- Each statement is a single atomic operation, so two users hitting the page at the same moment cannot lose a count.
INSERT IGNORE
INSERT IGNORE silently skips rows that would cause a duplicate-key error and inserts the rest:
INSERT IGNORE INTO departments (dept_id, dept_name, location)VALUES (1, 'Engineering', 'Chennai'), (6, 'Legal', 'Delhi');SHOW WARNINGS;SELECT * FROM departments;| Level | Code | Message |
|---|---|---|
| Warning | 1062 | Duplicate entry '1' for key 'departments.PRIMARY' |
| dept_id | dept_name | location |
|---|---|---|
| 1 | Engineering | Bengaluru |
| 2 | Sales | Mumbai |
| 3 | Marketing | Pune |
| 4 | HR | Delhi |
| 5 | Finance | Mumbai |
| 6 | Legal | Delhi |
- Department 1 already exists, so that row is skipped and becomes a warning; its location stays Bengaluru.
- Department 6 is new and is inserted: 1 row affected, 1 warning.
SHOW WARNINGSexplains which row was ignored.
IGNORE also turns other errors into warnings, such as values that are too long (they are cut short). Use it only when you really want “insert if new, otherwise do nothing”.
REPLACE INTO
REPLACE works like INSERT, but if the key exists it deletes the old row and inserts the new one:
REPLACE INTO customers (customer_id, customer_name, city)VALUES (6, 'Manoj Pillai', 'Kochi'); SELECT * FROM customers WHERE customer_id = 6;| customer_id | customer_name | city | joined_on | |
|---|---|---|---|---|
| 6 | Manoj Pillai | NULL | Kochi | NULL |
- Customer 6 exists, so MySQL deletes that row and inserts the new one: 2 rows affected.
- The new row has only the columns given.
emailandjoined_onwere not supplied, so the old values are lost and becomeNULL.
REPLACE fails if other tables reference the row through a foreign key, because it really deletes it. Customer 1 has orders:
REPLACE INTO customers (customer_id, customer_name, city) VALUES (1, 'Ananya Sharma', 'Pune');ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`company`.`orders`, CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`customer_id`))
Which one should I use?
| Tool | Row is new | Row exists | Typical use |
|---|---|---|---|
INSERT ... ON DUPLICATE KEY UPDATE | insert | update chosen columns | sync, counters, settings |
INSERT IGNORE | insert | do nothing (warning) | load data, skip duplicates |
REPLACE | insert | delete + insert | rarely; full overwrite of a standalone table |
In other databases the same idea is INSERT ... ON CONFLICT DO UPDATE (PostgreSQL, SQLite) or MERGE (SQL Server, Oracle).
Exercise: Upsert stock levels
Products 3 and 11 arrive: product 3 (USB-C Hub, Electronics, 1999) with 30 units, product 11 (Sticky Notes, Stationery, 99) with 500 units. Write one statement that inserts new products and, for existing ones, sets stock to the new value. Show both products.
| product_id | product_name | stock |
|---|---|---|
| 3 | USB-C Hub | 30 |
| 11 | Sticky Notes | 500 |
Hint 1
Use a row alias and update only stock.
INSERT INTO products (product_id, product_name, category, price, stock)VALUES (3, 'USB-C Hub', 'Electronics', 1999, 30), (11, 'Sticky Notes', 'Stationery', 99, 500) AS newON DUPLICATE KEY UPDATE stock = new.stock; SELECT product_id, product_name, stock FROM products WHERE product_id IN (3, 11);| product_id | product_name | stock |
|---|---|---|
| 3 | USB-C Hub | 30 |
| 11 | Sticky Notes | 500 |
Quick quiz
1. What triggers the UPDATE part of ON DUPLICATE KEY UPDATE?
Only a unique key violation counts as a duplicate.
2. How many rows does MySQL report as affected when ON DUPLICATE KEY UPDATE changes one existing row?
An update through ON DUPLICATE KEY counts as 2 affected rows.
3. What happens to columns you do not list in a REPLACE?
REPLACE inserts a brand-new row.
Upsert FAQ
How do I insert or update in MySQL?
Use INSERT INTO t (...) VALUES (...) AS new ON DUPLICATE KEY UPDATE col = new.col; It inserts new rows and updates rows whose primary or unique key already exists.
What is the difference between REPLACE and INSERT ON DUPLICATE KEY UPDATE?
REPLACE deletes the existing row and inserts a new one, losing unlisted columns and firing delete triggers. ON DUPLICATE KEY UPDATE changes only the columns you name in the existing row.
Does MySQL support MERGE?
No. MySQL uses INSERT ... ON DUPLICATE KEY UPDATE instead of the standard MERGE statement.
Is INSERT IGNORE safe?
It is safe for skipping duplicate keys, but it also hides other problems by turning errors into warnings. Check SHOW WARNINGS after using it.