Как мне ОБНОВИТЬ строку в таблице или ВСТАВИТЬ ее, если она не существует?

84

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

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

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

Если возможно, SQL должен работать без изменений на SQLite, PostgreSQL и MySQL.

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

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

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

  • В MySQL есть ON DUPLICATE KEY UPDATEпункт.

РЕДАКТИРОВАТЬ: Спасибо за все отличные ответы. Похоже, Пол прав, и нет единого портативного способа сделать это. Для меня это довольно удивительно, так как это звучит как очень простая операция.

Реми Бланк
источник
6
Вы не найдете единого решения, которое работало бы для всех этих СУБД. Сожалею.
Пол Томблин,
1
возможный дубликат SQLite - UPSERT * not * INSERT или REPLACE
PearsonArtPhoto
возможный дубликат решений для INSERT OR UPDATE на SQL Server
Джонатан Леффлер,

Ответы:

138

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

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

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

Andygeers
источник
13
На самом деле, если быть точным, MySQL REPLACE всегда выполняет вставку, но сначала удаляет строку, если она уже существует. dev.mysql.com/doc/refman/4.1/en/replace.html
Эван,
91
Важно понимать, что это вставка + удаление и никогда и обновление. Следствием этого является то, что вам всегда нужно быть уверенным, что при замене вы всегда должны включать данные для всех полей.
Zoredache
2
@Zoredache Технически, это «удалить, затем вставить», поскольку (n) «вставить + удалить» фактически то же самое, что и удаление, но это расщепляет волосы.
Agi Hammerthief
2
@Agihammerthief есть очень реальная разница, а именно то, что вновь вставленная строка НЕ ​​будет иметь тот же первичный ключ, что и строка, которая была удалена. С ON DUPLICATE это будет тот же первичный ключ (если вы специально не измените его).
Тим Стрейдхорст,
32

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

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

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

REPLACE INTO [...blah...]

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

Кайл Кронин
источник
Это требует, чтобы вы определили a PRAMARY KEYв своих values.
DawnSong
25

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

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

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

jmoz
источник
2
Этот ответ, честно говоря, должен быть выбранным. Это неразрушающее изменение, если вы не отправляете все поля в запись.
Jordan
9

предложение 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)
;
Огненная ворона
источник
4
REPLACE является атомарным, поэтому нет точки, в которой строка не существует.
Brilliand
5

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

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

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

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

Просто помните, что это не защита от ошибок, и в конечном итоге она выйдет из строя.


источник
4

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

Джонатан Леффлер
источник
0

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

Ши
источник
0

Не могли бы вы использовать триггер вставки? В случае неудачи сделайте обновление.

Майкл Тодд
источник
Триггер (по крайней мере, в PostgreSQL) запущен, когда команда сработала. т.е. у вас не может быть триггера, который запускается при сбое базовой команды.
0

Если вы согласны с использованием библиотеки, которая пишет за вас 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.

Шеймус Абшер
источник