Как обновить строку в таблице или вставить ее, если она не существует?

У меня есть следующая таблица счетчиков:

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

Я хотел бы увеличить один из счетчиков или установить его на ноль, если соответствующая строка еще не существует. Есть ли способ сделать это без проблем concurrency в стандартном SQL? Операция иногда является частью транзакции, иногда отдельной.

SQL должен запускаться без изменений в SQLite, PostgreSQL и MySQL, если это возможно.

Поиск дал несколько идей, которые либо страдают от проблем concurrency, либо специфичны для базы данных:

  • Попробуйте INSERT новую строку и UPDATE, если произошла ошибка. К сожалению, ошибка в INSERT прерывает текущую транзакцию.

  • UPDATE строка, и если строки не были изменены, INSERT новая строка.

  • MySQL имеет предложение ON DUPLICATE KEY UPDATE.

EDIT: Спасибо за все замечательные ответы. Похоже, что Пол прав, и нет ни одного, портативного способа сделать это. Это удивительно для меня, поскольку это звучит как очень простая операция.

Ответ 1

MySQL (и впоследствии SQLite) также поддерживает синтаксис REPLACE INTO:

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

Это автоматически идентифицирует первичный ключ и находит соответствующую строку для обновления, вставляя новую, если ни один не найден.

Ответ 2

SQLite поддерживает замену строки, если она уже существует:

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

Вы можете сократить это до

REPLACE INTO [...blah...]

Этот ярлык был добавлен для совместимости с выражением MySQL REPLACE INTO.

Ответ 3

Я бы сделал что-то вроде следующего:

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

Установка значения генерации в 0 в коде или в sql, но с использованием ON DUP... для увеличения значения. Я думаю, что синтаксис в любом случае.

Ответ 5

предложение ON DUPLICATE KEY UPDATE является лучшим решением, потому что: REPLACE делает DELETE, за которым следует INSERT, поэтому в течение столь незначительного периода запись удаляется, создавая настолько небольшую вероятность, что запрос может вернуться, если пропустить это, если страница просматривалась во время запроса REPLACE.

Я предпочитаю INSERT... ON DUPLICATE UPDATE... по этой причине.

Решение jmoz - лучшее: хотя я предпочитаю синтаксис SET в круглые скобки

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

Ответ 6

В PostgreSQL нет команды слияния, и на самом деле писать ее нетривиально - на самом деле есть странные крайние случаи, которые делают задачу "интересной".

Лучшим (как в: работе в наиболее возможных условиях) подходом является использование функции - например, показанной в manual (merge_db).

Если вы не хотите использовать функцию, вы обычно можете уйти с:

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

Просто помните, что это не ошибка, и она будет терпеть неудачу.

Ответ 7

Стандартный SQL предоставляет оператор MERGE для этой задачи. Не все СУБД поддерживают оператор MERGE.

Ответ 8

Если у вас нет общего способа атомарного обновления или вставки (например, через транзакцию), вы можете вернуться к другой схеме блокировки. 0-байтовый файл, системный мьютекс, именованный канал и т.д.

Ответ 9

Не могли бы вы использовать триггер insert? Если это не удается, выполните обновление.

Ответ 10

Если вы в порядке с использованием библиотеки, которая пишет SQL для вас, вы можете использовать Upsert (в настоящее время Ruby и Python только):

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

Это работает через MySQL, Postgres и SQLite3.

Он записывает хранимую процедуру или пользовательскую функцию (UDF) в MySQL и Postgres. Он использует INSERT OR REPLACE в SQLite3.