Upsert Multiple Rows in SQLite, PostgreSQL, and MySQL

databasemysqlpostgresqlsqlsqlite

January 30, 2025  |  2 min read

TLDR

  • SQLite and PostGreSQL: Use EXCLUDED to reference the would-have-been inserted row
  • MySQL: Use VALUES() function to reference the would-have-been inserted row

Syntax

-- sqlite
-- psql
INSERT INTO users (email, last_name, first_name)
VALUES
    ('john.doe@example.com', 'Doe', 'John'),
    ('jane.doe@example.com', 'Doe', 'Jane')
ON CONFLICT (email) DO UPDATE SET
    last_name = EXCLUDED.last_name,
    first_name = EXCLUDED.first_name;
 
-- mysql
INSERT INTO users (email, last_name, first_name)
VALUES
    ('john.doe@example.com', 'Doe', 'John'),
    ('jane.doe@example.com', 'Doe', 'Jane')
ON DUPLICATE KEY UPDATE
    last_name = VALUES(last_name),
    first_name = VALUES(first_name);

Gotchas

Upsert Might not Work Like How You Thought It Would

Per documents, the such ONLY works on columns with an uniqueness constraint, like UNIQUE or PRIMARY KEY. And, the upsert flow for each row is decided separately.

Should other qualifiers, like NOT NULL, fail to pass, the row insertion/update would still fail.

With hindsight, this was already stated in MySQL's "DUPLICATE KEY but not having to specify which" part.

Unwanted Row Locks

The affected rows will be locked during the query12, even though they might not be updated in the end. Treat them with care.

Anecdote

SQLite follows PostgreSQL in its upsert syntax, and it is documented.


Refs

Footnotes

  1. PostgreSQL reference on the lock

  2. MySQL reference on the lock