Several months ago I learned from an answer on Stack Overflow how to perform multiple updates at once in MySQL using the following syntax:
INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);
I've now switched over to PostgreSQL and apparently this is not correct. It's referring to all the correct tables so I assume it's a matter of different keywords being used but I'm not sure where in the PostgreSQL documentation this is covered.
To clarify, I want to insert several things and if they already exist to update them.
Personally, I've set up a "rule" attached to the insert statement. Say you had a "dns" table that recorded dns hits per customer on a per-time basis:
You wanted to be able to re-insert rows with updated values, or create them if they didn't exist already. Keyed on the customer_id and the time. Something like this:
Update: This has the potential to fail if simultaneous inserts are happening, as it will generate unique_violation exceptions. However, the non-terminated transaction will continue and succeed, and you just need to repeat the terminated transaction.
However, if there are tons of inserts happening all the time, you will want to put a table lock around the insert statements: SHARE ROW EXCLUSIVE locking will prevent any operations that could insert, delete or update rows in your target table. However, updates that do not update the unique key are safe, so if you no operation will do this, use advisory locks instead.
Also, the COPY command does not use RULES, so if you're inserting with COPY, you'll need to use triggers instead.
According the PostgreSQL documentation of the
INSERT
statement, handling theON DUPLICATE KEY
case is not supported. That part of the syntax is a proprietary MySQL extension.PostgreSQL since version 9.5 has UPSERT syntax, with ON CONFLICT clause. with the following syntax (similar to MySQL)
Searching postgresql's email group archives for "upsert" leads to finding an example of doing what you possibly want to do, in the manual:
There's possibly an example of how to do this in bulk, using CTEs in 9.1 and above, in the hackers mailing list:
See a_horse_with_no_name's answer for a clearer example.
UPDATE will return the number of modified rows. If you use JDBC (Java), you can then check this value against 0 and, if no rows have been affected, fire INSERT instead. If you use some other programming language, maybe the number of the modified rows still can be obtained, check documentation.
This may not be as elegant but you have much simpler SQL that is more trivial to use from the calling code. Differently, if you write ten line script in PL/PSQL, you probably should have unit test of one or another kind just for it.
For merging small sets, using the above function is fine. However, if you are merging large amounts of data, I'd suggest looking into http://mbk.projects.postgresql.org
The current best practice that I'm aware of is:
In PostgreSQL 9.5 and newer you can use
INSERT ... ON CONFLICT UPDATE
.See the documentation.
A MySQL
INSERT ... ON DUPLICATE KEY UPDATE
can be directly rephrased to aON CONFLICT UPDATE
. Neither is SQL-standard syntax, they're both database-specific extensions. There are good reasonsMERGE
wasn't used for this, a new syntax wasn't created just for fun. (MySQL's syntax also has issues that mean it wasn't adopted directly).e.g. given setup:
the MySQL query:
becomes:
Differences:
You must specify the column name (or unique constraint name) to use for the uniqueness check. That's the
ON CONFLICT (columnname) DO
The keyword
SET
must be used, as if this was a normalUPDATE
statementIt has some nice features too:
You can have a
WHERE
clause on yourUPDATE
(letting you effectively turnON CONFLICT UPDATE
intoON CONFLICT IGNORE
for certain values)The proposed-for-insertion values are available as the row-variable
EXCLUDED
, which has the same structure as the target table. You can get the original values in the table by using the table name. So in this caseEXCLUDED.c
will be10
(because that's what we tried to insert) and"table".c
will be3
because that's the current value in the table. You can use either or both in theSET
expressions andWHERE
clause.For background on upsert see How to UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) in PostgreSQL?