SQL example

How to insert or update if a row exists in SQL (upsert)

8 min read Updated Oct 2026 Runs in an isolated runtime
Quick answer

Add an ON CONFLICT clause to the insert: INSERT INTO products (sku, price) VALUES ('A1', 9) ON CONFLICT (sku) DO UPDATE SET price = excluded.price. A new key inserts a row; an existing key updates that row, and excluded holds the values you tried to insert. Use DO NOTHING instead to insert only if the row doesn't exist. This works in SQLite 3.24+ and PostgreSQL; MySQL uses ON DUPLICATE KEY UPDATE and SQL Server uses MERGE.

"Upsert" means insert a row, or update it if a row with the same key is already there. Doing it as a SELECT followed by an INSERT or an UPDATE takes two round trips and can race with another session doing the same thing, so most databases have a single statement for it. The standard-looking one, and the one shown first, is INSERT … ON CONFLICT. The examples run on SQLite; section 7 gives the PostgreSQL, MySQL and SQL Server forms. If you only need to add many rows, see insert multiple rows, and to copy rows in from another table, see insert from a SELECT. Each example runs on this page: hit Run, then edit the SQL and run it again.

1INSERT … ON CONFLICT DO UPDATERecommended

Write the insert as usual, then name the key that may collide in ON CONFLICT (sku) and say what to change in DO UPDATE SET. Inside the SET, the special table excluded is the row that failed to insert, so price = excluded.price copies the new price over the old one. Each row of a multi-row VALUES is decided on its own: some can update while others insert.

upsert.sql

Output

A1 keeps its row and its stock of 12, but now reads Big mug at 9; C3 is new, with the default stock of 0; B2 wasn't in the insert and is untouched. Only the columns listed in SET change on an update, so leave out any column you want to keep, such as a created-at date. This syntax needs SQLite 3.24 or later (2018) and PostgreSQL 9.5 or later. In SQLite, an upsert fed by INSERT … SELECT needs a WHERE true after the SELECT; without it the parser reads ON as part of a join and fails with near "DO": syntax error.

2A counter: insert 1, or add 1

The SET can compute the new value from the old one. That turns an upsert into a counter: the first event for a key inserts a row with 1, and every later event adds 1 to it. Qualify the old value with the table name, page_views.views; SQLite accepts a bare views, but PostgreSQL rejects it as ambiguous. Add RETURNING (SQLite 3.35+) to get the row back after the insert or update.

counter.sql

Output

Three visits to /home leave it at 3 and one visit to /docs leaves it at 1. RETURNING prints /home with 3, the value after the update, so you can read the new count without a second query. Because the whole thing is one statement, two sessions counting at once can't both insert the first row; one of them takes the update path. Adding excluded.views instead of 1 turns the same pattern into a running total.

3Insert only if the row doesn't exist: DO NOTHING

When an existing row should win, replace DO UPDATE … with DO NOTHING. A clash on the key skips that row without an error, and the rest of the statement carries on. changes() shows how many rows the last statement inserted. Leaving out the target, as in plain ON CONFLICT DO NOTHING, skips a clash on any primary key or unique column.

do-nothing.sql

Output

The second insert for [email protected] inserts 0 rows, so Ana keeps her name. Ben is new and goes in with id 2, and the INSERT OR IGNORE that follows changes nothing because his email is now taken. INSERT OR IGNORE is SQLite-only and also ignores NOT NULL and CHECK failures, so prefer ON CONFLICT DO NOTHING, which skips only key clashes and also runs on PostgreSQL.

4The conflict target must be a PRIMARY KEY or UNIQUE constraint

ON CONFLICT only fires when an insert breaks a uniqueness rule, so the columns in the parentheses must match a PRIMARY KEY, a UNIQUE constraint or a unique index exactly. For a key made of several columns, list all of them. Naming a column with no such constraint isn't a silent no-op: the statement fails, as the last insert shows.

conflict-target.sql

Output

The (shop, item) pair adds 3 to the 5 pens at north, giving 8, and inserts the 7 pens at south as a new row. The last statement fails with ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint. If your table has no such constraint yet, add one with CREATE UNIQUE INDEX … ON stock (shop, item) before you upsert; without it the database has nothing to detect a duplicate with.

5Why INSERT OR REPLACE is not an upsert

SQLite's INSERT OR REPLACE (also spelled REPLACE INTO) looks like an upsert but works differently: on a clash it deletes the old row and inserts a brand new one. Every column you didn't mention falls back to its default, and an INTEGER PRIMARY KEY gets a new value. The embed replaces A1 one way and updates B2 the other way.

replace.sql

Output

After the replace, A1 has moved from id 1 to id 3 and its stock of 12 has dropped to 0, the column default, because the statement didn't mention stock. The upsert on B2 changes only the price to 16 and keeps id 2 and stock 4. The delete is a real delete, so it also fires delete triggers and ON DELETE CASCADE: with PRAGMA foreign_keys = ON, replacing a product this way removes its reviews too. Use INSERT OR REPLACE only when the new row really should replace every column of the old one; for "update if it exists", use ON CONFLICT DO UPDATE.

6The portable two-step: INSERT … SELECT … WHERE NOT EXISTS

Every database can insert a row only when no match exists, by inserting from a SELECT that returns one row or none. WHERE NOT EXISTS (…) decides which. For "update or insert", run an UPDATE first and then the same guarded INSERT: if the update found the row, the insert adds nothing. The same NOT EXISTS test is how you find rows with no match in another table.

not-exists.sql

Output

Ben is inserted because no row had his email; Ana's row is updated to Ana B and the guarded insert after it adds nothing. The same two statements ran unchanged on PostgreSQL, MySQL and SQL Server (Babelfish). The catch is concurrency: between the check and the insert, another session can add the same key. Keep a PRIMARY KEY or UNIQUE constraint on the column so the loser of that race gets an error instead of a duplicate row, and use the one-statement upsert wherever your database has one.

7Upsert in PostgreSQL, MySQL and SQL Server

Each database spells the upsert its own way. Every row below was run on PostgreSQL 18, MySQL 8.4 or SQL Server's T-SQL through Babelfish, except where the notes say otherwise. Oracle has no ON CONFLICT; its upsert is MERGE.

SyntaxDatabaseNotes
INSERT … ON CONFLICT (key) DO UPDATE SET col = excluded.colPostgreSQL 9.5+ (and SQLite 3.24+)Same as this page; write table.col for the old value, a bare name is ambiguous
INSERT … ON CONFLICT DO NOTHINGPostgreSQL 9.5+ (and SQLite 3.24+)Insert if not exists; skipped rows report INSERT 0 0
INSERT … VALUES (…) AS new ON DUPLICATE KEY UPDATE col = new.colMySQL 8.0.19+The current form; no conflict target, any unique key triggers it
ON DUPLICATE KEY UPDATE col = VALUES(col)MySQL, MariaDBStill runs on 8.4 but warns 1287: VALUES() is deprecated
INSERT IGNORE INTO …MySQLSkips duplicates, but also turns NULL and too-long values into warnings
MERGE INTO t USING s ON … WHEN MATCHED … WHEN NOT MATCHED …SQL Server, PostgreSQL 15+Ran on PostgreSQL. Not run for SQL Server: Babelfish doesn't support MERGE
IF EXISTS (…) UPDATE … ELSE INSERT …SQL ServerTwo statements; wrap in a transaction with UPDLOCK, HOLDLOCK under load
UPDATE …; IF @@ROWCOUNT = 0 INSERT …SQL ServerUpdate first, insert when no row changed; one lookup fewer
INSERT … SELECT … WHERE NOT EXISTS (…)All fourPortable insert if not exists, as in section 6

Frequently asked questions

How do I insert a row only if it does not already exist?

Put a PRIMARY KEY or UNIQUE constraint on the column, then use INSERT … ON CONFLICT (email) DO NOTHING in SQLite or PostgreSQL, INSERT IGNORE in MySQL, or IF NOT EXISTS (SELECT 1 FROM users WHERE email = …) INSERT … in SQL Server. The portable form is INSERT INTO users (email, name) SELECT …, … WHERE NOT EXISTS (SELECT 1 FROM users WHERE email = …), which ran unchanged on all four databases.

What is excluded in ON CONFLICT DO UPDATE?

excluded is a pseudo-table holding the row you tried to insert, the one that clashed. In ON CONFLICT (sku) DO UPDATE SET price = excluded.price, excluded.price is the new value and products.price is the value already in the table, so SET qty = stock.qty + excluded.qty adds the incoming amount to the stored one.

What is the difference between INSERT OR REPLACE and an upsert?

INSERT OR REPLACE (and MySQL REPLACE INTO) deletes the clashing row and inserts a new one. Columns you leave out reset to their defaults, an INTEGER PRIMARY KEY gets a new id (A1 went from id 1 to 3 in section 5), and ON DELETE CASCADE removes child rows. ON CONFLICT DO UPDATE changes only the columns you list and keeps the row.

How do I do an INSERT or UPDATE in SQL Server?

Use MERGE INTO target USING source ON target.key = source.key WHEN MATCHED THEN UPDATE SET … WHEN NOT MATCHED THEN INSERT …;, or two statements: UPDATE … followed by IF @@ROWCOUNT = 0 INSERT …. With many sessions writing at once, run the pair in a transaction with WITH (UPDLOCK, HOLDLOCK) on the update so two sessions cannot both insert. Babelfish answers MERGE with 'MERGE' is not currently supported in Babelfish, so only the two-statement form was run here.

Why does ON CONFLICT say it does not match any PRIMARY KEY or UNIQUE constraint?

The columns in ON CONFLICT (…) must be exactly the columns of a primary key, a unique constraint or a unique index. Naming any other column fails with ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint. Add one with CREATE UNIQUE INDEX idx_name ON t (col), or list every column of a composite key, such as ON CONFLICT (shop, item).

Can I upsert from a SELECT in SQLite?

Yes, but add WHERE true after the SELECT: INSERT INTO t SELECT k, v FROM src WHERE true ON CONFLICT (k) DO UPDATE SET v = excluded.v. Without it SQLite reads ON as a join condition and reports near "DO": syntax error. PostgreSQL needs no such workaround.

Run it yourself

Open any of these in the full SQLite editor to tweak, run and share.

SQLite playground