The general suggested way for handling UPSERT situations is one of the following:
- Try to do an INSERT. On error, have the application retry with an UPDATE.
- Write a PL/pgSQL procedure which does insert-then-update or update-then-insert.
What's the method? Using writeable WITH clauses. This feature, introduced in 9.1, allows you to do a multi-step write transaction as a single query. For an example, let's construct a dummy table with a unique key on ID and a value column, then populate it:
create table test_upsert ( id int not null primary key,
val text );
insert into test_upsert select i, 'aaa'
from generate_series (1, 100) as gs(i);
Now, let's say we wanted to update ID 50, or insert it if it doesn't exist. We can do that like so:
WITH
newrow ( id, val ) as (
VALUES ( 50::INT, 'bbb'::TEXT ) ),
tryupdate as (
UPDATE test_upsert SET val = newrow.val
FROM newrow
WHERE test_upsert.id = newrow.id
RETURNING test_upsert.id
)
INSERT INTO test_upsert
SELECT id, val
FROM newrow
WHERE id NOT IN ( SELECT id FROM tryupdate );
The above tries to update ID=50. If no rows are updated, it inserts them. This also works for multiple rows:
WITH
newrow ( id, val ) as (
VALUES ( 75::INT, 'ccc'::TEXT ),
( 103::INT, 'ccc'::TEXT )
),
tryupdate as (
UPDATE test_upsert SET val = newrow.val
FROM newrow
WHERE test_upsert.id = newrow.id
RETURNING test_upsert.id
)
INSERT INTO test_upsert
SELECT id, val
FROM newrow
WHERE id NOT IN ( SELECT id FROM tryupdate );
... and will update or insert each row as called for.
Given that we can do the above, why do we need real UPSERT? Well, there's some problems with this approximate method:
- It's not concurrency-safe, and can produce unexpected results given really bad timing of multiple connections wanting to update the same rows.
- It will still produce key violation errors given bad concurrency timing, just fewer of them than method 1 above.
- It's still higher overhead than 9.5's UPSERT feature, which is optimized.
- It will return INSERT 0 0 from calls that do only updates, possibly making the app think the upsert failed.
- It's not safe to use with INSERT/UPDATE triggers on the same table.
No comments:
Post a Comment