SQL data modification
Insert or update: ON CONFLICT
The idea in 30 seconds
"Insert this row, or update it if it already exists" is so common it has a name: upsert. In PostgreSQL it is INSERT ... ON CONFLICT. It needs a unique key to decide what "already exists" means, and it does the check and the write in one statement instead of two.
Helps to know first: SQL basics: reading data
On this page
The problem
A supplier sends a daily price list. For each product, you want to add it if it is new and update its price if you already sell it. The obvious approach is two steps: check whether the product exists, then INSERT or UPDATE. That means two round trips, and code that has to handle a product appearing between the check and the write.
Without help, inserting a product that already exists is simply an error:
INSERT INTO products (id, name, category, price) VALUES (2, 'Wireless mouse', 'Accessories', 1399.00);
ERROR: duplicate key value violates unique constraint "products_pkey"
DETAIL: Key (id)=(2) already exists.
ON CONFLICT DO UPDATE
ON CONFLICT tells PostgreSQL what to do instead of raising that error. Inside DO UPDATE, the special name EXCLUDED means “the row I tried to insert”:
BEGIN;
INSERT INTO products (id, name, category, price)
VALUES (2, 'Wireless mouse', 'Accessories', 1299.00)
ON CONFLICT (id) DO UPDATE SET price = EXCLUDED.price
RETURNING id, name, price;
ROLLBACK;
id | name | price
----+----------------+---------
2 | Wireless mouse | 1299.00
Product 2 already existed, so its price changed from 1,200 to 1,299. Had it not existed, the same statement would have inserted it. Unlike check-then-write in application code, PostgreSQL guarantees one of those two outcomes even when many sessions run the same upsert at once.
(id) is the conflict target: the unique column or columns that decide whether a row “already exists”. It must match a primary key, a unique constraint or a unique index.
ON CONFLICT DO NOTHING
Sometimes the right answer to “it already exists” is to leave it alone:
BEGIN;
INSERT INTO customers (id, name, email, joined_on)
VALUES (10, 'Asha again', 'asha@example.com', '2025-10-18')
ON CONFLICT (email) DO NOTHING
RETURNING id;
ROLLBACK;
id
----
(0 rows)
INSERT 0 0
asha@example.com is taken, so nothing was inserted. RETURNING came back empty and the count is 0: that is how your code can tell the row was skipped.
Check yourself
What does
EXCLUDED.pricerefer to inON CONFLICT … DO UPDATE SET price = EXCLUDED.price?Show answer
The price in the row you tried to insert, as opposed to the price already stored in the table.
Why does
ON CONFLICT (email)needemailto be unique?Show answer
The conflict target must match a primary key, unique constraint or unique index. Without one, PostgreSQL has no rule for deciding that two rows are “the same”.
How can your code tell whether
ON CONFLICT DO NOTHINGinserted the row or skipped it?Show answer
The affected row count is 1 or 0, and
RETURNINGreturns the inserted row or nothing.
Finished reading?
Saved in this browser only.
Sources
Where each claim on this page comes from.
- 1.PostgreSQL documentation: INSERTPrimary· ON CONFLICT Clause
ON CONFLICT replaces a unique-violation error with an alternative action, DO NOTHING or DO UPDATE; the conflict target names the unique columns; EXCLUDED refers to the row proposed for insertion.
- 2.PostgreSQL documentation: 6.4 Returning Data from Modified RowsPrimary· §6.4
RETURNING returns data from the rows a statement modified.
Go further
What to read next, and why it is worth the time.
- 1.PostgreSQL documentation: INSERTPrimary· ON CONFLICT Clause, and Examples
The full rules, including the guarantees ON CONFLICT DO UPDATE gives under concurrent inserts.
Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.