85

I have the following table of counters:

CREATE TABLE cache (
    key text PRIMARY KEY,
    generation int
);

I would like to increment one of the counters, or set it to zero if the corresponding row doesn't exist yet. Is there a way to do this without concurrency issues in standard SQL? The operation is sometimes part of a transaction, sometimes separate.

The SQL must run unmodified on SQLite, PostgreSQL and MySQL, if possible.

A search yielded several ideas which either suffer from concurrency issues, or are specific to a database:

  • Try to INSERT a new row, and UPDATE if there was an error. Unfortunately, the error on INSERT aborts the current transaction.

  • UPDATE the row, and if no rows were modified, INSERT a new row.

  • MySQL has an ON DUPLICATE KEY UPDATE clause.

EDIT: Thanks for all the great replies. It looks like Paul is right, and there's not a single, portable way of doing this. That's quite surprising to me, as it sounds like a very basic operation.

mu is too short
  • 396,305
  • 64
  • 779
  • 743
Remy Blank
  • 4,136
  • 2
  • 20
  • 23
  • 6
    You're not going to find a single solution that works for all these RDBMS. Sorry. – Paul Tomblin Mar 27 '09 at 17:22
  • 1
    possible duplicate of [SQLite - UPSERT \*not\* INSERT or REPLACE](http://stackoverflow.com/questions/418898/sqlite-upsert-not-insert-or-replace) – PearsonArtPhoto Nov 12 '12 at 23:28
  • possible duplicate of [Solutions for INSERT OR UPDATE on SQL Server](http://stackoverflow.com/questions/108403/solutions-for-insert-or-update-on-sql-server) – Jonathan Leffler Nov 12 '12 at 23:30

10 Answers10

140

MySQL (and subsequently SQLite) also support the REPLACE INTO syntax:

REPLACE INTO my_table (pk_id, col1) VALUES (5, '123');

This automatically identifies the primary key and finds a matching row to update, inserting a new one if none is found.

andygeers
  • 6,597
  • 9
  • 45
  • 63
  • 13
    Actually, to be specific, MySQL's REPLACE always does an insert, but it will delete the row if it already exists, first. http://dev.mysql.com/doc/refman/4.1/en/replace.html – Evan Mar 28 '09 at 03:57
  • 94
    It is important to understand that it is an insert+delete and never and update. The consequence of this, is that you will always want to make sure when you do a replace, you should always include data for all the fields. – Zoredache Mar 28 '09 at 07:39
  • 2
    @Zoredache Technically, it's a 'delete, then insert', as a(n) 'insert + delete' is effectively the same as a delete but that's splitting hairs. – Agi Hammerthief Sep 16 '14 at 11:52
  • 2
    @Agihammerthief there is a very real difference namely that the newly inserted row will NOT have the same primary key as the row that has been deleted. With ON DUPLICATE it will be the same primary key (unless you specifically change it). – Tim Strijdhorst Dec 20 '16 at 11:04
33

SQLite supports replacing a row if it already exists:

INSERT OR REPLACE INTO [...blah...]

You can shorten this to

REPLACE INTO [...blah...]

This shortcut was added to be compatible with the MySQL REPLACE INTO expression.

Kyle Cronin
  • 72,761
  • 40
  • 144
  • 160
26

I would do something like the following:

INSERT INTO cache VALUES (key, generation)
ON DUPLICATE KEY UPDATE (key = key, generation = generation + 1);

Setting the generation value to 0 in code or in the sql but the using the ON DUP... to increment the value. I think that's the syntax anyway.

jmoz
  • 7,017
  • 4
  • 29
  • 31
  • 2
    This answer should honestly be the selected answer. It's a non-destructive edit if you aren't submitting all fields to an entry. – Jordan Apr 09 '18 at 21:37
9

the ON DUPLICATE KEY UPDATE clause is the best solution because: REPLACE does a DELETE followed by an INSERT so for an ever so slight period the record is removed creating the ever so slight possibility that a query could come back having skipped that if the page was viewed during the REPLACE query.

I prefer INSERT ... ON DUPLICATE UPDATE ... for that reason.

jmoz's solution is the best: though I prefer the SET syntax to the parentheses

INSERT INTO cache 
SET key = 'key', generation = 'generation'
ON DUPLICATE KEY 
UPDATE key = 'key', generation = (generation + 1)
;
Fire Crow
  • 6,921
  • 4
  • 32
  • 34
8

I don't know that you are going to find a platform-neutral solution.

This is commonly called an "UPSERT".

See some related discussions:

Community
  • 1
  • 1
BradC
  • 37,320
  • 12
  • 65
  • 90
5

In PostgreSQL there is no merge command, and actually writing it is not trivial - there are actually strange edge cases that make the task "interesting".

The best (as in: working in the most possible conditions) approach, is to use function - such as one shown in manual (merge_db).

If you don't want to use function, you can usually get away with:

updated = db.execute(UPDATE ... RETURNING 1)
if (!updated)
  db.execute(INSERT...)

Just remember that it is not fault proof and it will fail eventually.

4

Standard SQL provides the MERGE statement for this task. Not all DBMS support the MERGE statement.

Jonathan Leffler
  • 666,971
  • 126
  • 813
  • 1,185
0

If you're OK with using a library that writes the SQL for you, then you can use Upsert (currently Ruby and Python only):

Pet.upsert({:name => 'Jerry'}, :breed => 'beagle')
Pet.upsert({:name => 'Jerry'}, :color => 'brown')

That works across MySQL, Postgres, and SQLite3.

It writes a stored procedure or user-defined function (UDF) in MySQL and Postgres. It uses INSERT OR REPLACE in SQLite3.

Seamus Abshere
  • 7,730
  • 2
  • 40
  • 59
0

If you don't have a common way to atomically update or insert (e.g., via a transaction) then you can fallback to another locking scheme. A 0-byte file, system mutex, named pipe, etc...

Shea
  • 10,649
  • 2
  • 16
  • 20
0

Could you use an insert trigger? If it fails, do an update.

Michael Todd
  • 15,795
  • 4
  • 46
  • 69
  • Trigger (at least in PostgreSQL) is running when the command worked. i.e. you can't have trigger that runs when base command failed. –  Mar 27 '09 at 17:43