SQLite UPSERT / ОБНОВЛЕНИЕ ИЛИ ВСТАВКА


104

Мне нужно выполнить UPSERT / INSERT OR UPDATE для базы данных SQLite.

Есть команда INSERT OR REPLACE, которая во многих случаях может быть полезной. Но если вы хотите сохранить свой идентификатор с автоинкрементом из-за внешних ключей, он не работает, поскольку он удаляет строку, создает новую и, следовательно, эта новая строка имеет новый идентификатор.

Это будет таблица:

игроки - (первичный ключ по id, имя_пользователя уникальное)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |

Ответы:


53

Это поздний ответ. Начиная с версии SQLIte 3.24.0, выпущенной 4 июня 2018 г., наконец, появилась поддержка предложения UPSERT , следующего за синтаксисом PostgreSQL.

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

Примечание. Для тех, кто использует версию SQLite ранее, чем 3.24.0, см. Этот ответ ниже (опубликованный мной, @MarqueIV).

Однако, если у вас есть возможность обновить, вам настоятельно рекомендуется сделать это, поскольку в отличие от моего решения, размещенное здесь обеспечивает желаемое поведение в одном операторе. Кроме того, вы получаете все другие функции, улучшения и исправления ошибок, которые обычно поставляются с более поздним выпуском.


На данный момент этого релиза в репозитории Ubuntu нет.
— bl79 09

Почему я не могу использовать это на Android? Я пробовал db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Выдает мне синтаксическую ошибку в слове «on»
— Bastian Voigt

1
@BastianVoigt Поскольку библиотеки SQLite3, установленные в различных версиях Android, старше 3.24.0. См .: developer.android.com/reference/android/database/sqlite/… К сожалению, если вам нужна новая функция SQLite3 (или любой другой системной библиотеки) на Android или iOS, вам необходимо связать определенную версию SQLite в своем приложение вместо того, чтобы полагаться на установленное системой.
— prapin

Разве это не UPSERT, а скорее INDATE, поскольку он сначала пытается вставить вставку? ;)
— Марк А. Донохо

@BastianVoigt, пожалуйста, посмотрите мой ответ ниже (ссылка на вопрос выше), который относится к версиям до 3.24.0.
— Марк А. Донохо

107

Стиль вопросов и ответов

Что ж, после нескольких часов изучения проблемы и борьбы с ней я обнаружил, что есть два способа решить эту проблему, в зависимости от структуры вашей таблицы и от того, активированы ли у вас ограничения внешних ключей для поддержания целостности. Я хотел бы поделиться этим в чистом формате, чтобы сэкономить время людям, которые могут оказаться в моей ситуации.


Вариант 1. Вы можете позволить себе удалить строку

Другими словами, у вас нет внешнего ключа, или, если он у вас есть, ваш механизм SQLite настроен так, что нет исключений целостности. Путь - ВСТАВИТЬ ИЛИ ЗАМЕНИТЬ . Если вы пытаетесь вставить / обновить игрока, чей идентификатор уже существует, механизм SQLite удалит эту строку и вставит предоставленные вами данные. Теперь возникает вопрос: что делать, чтобы старый идентификатор оставался связанным?

Допустим, мы хотим выполнить UPSERT с данными user_name = 'steven' и age = 32.

Взгляните на этот код:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

Хитрость заключается в слиянии. Он возвращает идентификатор пользователя «steven», если таковой имеется, а в противном случае возвращает новый свежий идентификатор.


Вариант 2. Вы не можете позволить себе удалить строку

Попробовав предыдущее решение, я понял, что в моем случае это может привести к уничтожению данных, поскольку этот идентификатор работает как внешний ключ для другой таблицы. Кроме того, я создал таблицу с предложением ON DELETE CASCADE , что означало бы, что он будет удалять данные без уведомления. Опасно.

Итак, я сначала подумал о предложении IF, но в SQLite есть только CASE . И этот СЛУЧАЙ нельзя использовать (или, по крайней мере, я не справился с этим) для выполнения одного запроса UPDATE, если СУЩЕСТВУЕТ (выберите идентификатор из игроков, где user_name = 'steven'), и INSERT, если это не так. Нет.

И вот, наконец, я успешно применил грубую силу. Логика заключается в том, что для каждого UPSERT, который вы хотите выполнить, сначала выполните INSERT OR IGNORE, чтобы убедиться, что есть строка с нашим пользователем, а затем выполните запрос UPDATE с точно такими же данными, которые вы пытались вставить.

Те же данные, что и раньше: user_name = 'steven' и age = 32.

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

И это все!

РЕДАКТИРОВАТЬ

Как прокомментировал Энди, попытка сначала вставить, а затем обновить может привести к срабатыванию триггеров чаще, чем ожидалось. На мой взгляд, это не проблема безопасности данных, но действительно, запуск ненужных событий не имеет смысла. Следовательно, улучшенное решение будет:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

10
То же самое ... вариант 2 великолепен. За исключением того, что я сделал наоборот: попробуйте обновить, проверьте, если rowsAffected> 0, если нет, то сделайте вставку.
— Tom Spencer

Это тоже неплохой подход, единственный небольшой недостаток в том, что у вас нет только одного SQL для «upsert».
— bgusach 05

2
вам не нужно повторно устанавливать user_name в операторе обновления в последнем примере кода. Достаточно указать возраст.
— Серг Стецук

72

Вот подход, который не требует «игнорирования» грубой силы, который будет работать только в случае нарушения ключа. Этот способ работает при любых условиях, указанных вами в обновлении.

Попробуй это...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

Как это устроено

«Волшебный соус» здесь используется Changes()в Whereпредложении. Changes()представляет количество строк, затронутых последней операцией, которая в данном случае является обновлением.

В приведенном выше примере, если после обновления нет никаких изменений (т. Е. Запись не существует), то Changes()= 0, поэтому Whereпредложение в Insertоператоре оценивается как истинное, и новая строка вставляется с указанными данными.

Если Update действительноChanges() обновил существующую строку, то = 1 (или, точнее, не ноль, если было обновлено более одной строки), поэтому предложение Where в Insertnow оценивается как false, и, таким образом, вставка не выполняется .

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

Кроме того, поскольку это просто стандартное Whereпредложение, оно может быть основано на чем угодно, а не только на ключевых нарушениях. Точно так же вы можете использовать Changes()в сочетании с любыми другими выражениями, которые вам нужны / разрешены.


1
Это отлично сработало для меня. Я не встречал этого решения где-либо еще вместе со всеми примерами INSERT OR REPLACE, оно намного более гибкое для моего варианта использования.
— csab

@MarqueIV, а что делать, если нужно обновить или вставить два элемента? например, первая обновилась, а вторая не существует. в таком случае Changes() = 0вернет false, а две строки сделают INSERT OR REPLACE
— Андрей Антонов

Обычно предполагается, что UPSERT действует с одной записью. Если вы говорите, что точно знаете, что он действует более чем на одну запись, измените проверку счетчика соответствующим образом.
— Марк А. Донохо

Плохо то, что если строка существует, метод обновления должен выполняться независимо от того, изменилась строка или нет.
— Jimi

1
Почему это плохо? И если данные не изменились, зачем вы вообще звоните UPSERT? Но даже в этом случае это хорошо , что обновление происходит, настройка, Changes=1иначе INSERTоператор будет неправильно запускаться, чего вы не хотите.
— Марк А. Донохо

25

Проблема со всеми представленными ответами заключается в полном отсутствии учета триггеров (и, возможно, других побочных эффектов). Решение вроде

INSERT OR IGNORE ...
UPDATE ...

приводит к выполнению обоих триггеров (для вставки, а затем для обновления), когда строка не существует.

Правильное решение

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

в этом случае выполняется только один оператор (если строка существует или нет).


1
Я понимаю вашу точку зрения. Я обновлю свой вопрос. Кстати, я не знаю, зачем UPDATE OR IGNOREэто нужно, поскольку обновление не выйдет из строя, если строки не найдены.
— bgusach

1
читаемость? Я могу сразу увидеть, что делает код Энди. Твой бгусач Пришлось минутку изучить, чтобы разобраться.
— Brandan

6

Чтобы иметь чистый UPSERT без дыр (для программистов), не использующих уникальные и другие ключи:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT changes () вернет количество обновлений, выполненных в последнем запросе. Затем проверьте, равно ли возвращаемое значение от changes () значение 0, если да, выполните:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 

Это эквивалентно тому, что @fiznool предложил в своем комментарии (хотя я бы выбрал его решение). Все в порядке и на самом деле работает нормально, но у вас нет уникального оператора SQL. UPSERT, не основанный на PK или других уникальных ключах, для меня практически не имеет смысла.
— bgusach 01

4

Вы также можете просто добавить предложение ON CONFLICT REPLACE к уникальному ограничению user_name, а затем просто INSERT прочь, предоставив SQLite выяснить, что делать в случае конфликта. См. Https://sqlite.org/lang_conflict.html .

Также обратите внимание на предложение о триггерах удаления: когда стратегия разрешения конфликтов REPLACE удаляет строки, чтобы удовлетворить ограничение, триггеры удаления срабатывают тогда и только тогда, когда рекурсивные триггеры включены.


2

Вариант 1: Вставить -> Обновить

Если вам нравится избегать и того, changes()=0и другого, и INSERT OR IGNOREдаже если вы не можете позволить себе удаление строки - вы можете использовать эту логику;

Сначала вставьте (если не существует), а затем обновите , отфильтровав уникальный ключ.

пример

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

Что касается триггеров

Примечание: я не тестировал, какие триггеры вызываются, но предполагаю следующее:

если строка не существует

  • ПЕРЕД ВСТАВКОЙ
  • INSERT, используя INSTEAD OF
  • ПОСЛЕ ВСТАВКИ
  • ПЕРЕД ОБНОВЛЕНИЕМ
  • ОБНОВЛЕНИЕ с использованием INSTEAD OF
  • ПОСЛЕ ОБНОВЛЕНИЯ

если строка существует

  • ПЕРЕД ОБНОВЛЕНИЕМ
  • ОБНОВЛЕНИЕ с использованием INSTEAD OF
  • ПОСЛЕ ОБНОВЛЕНИЯ

Вариант 2. Вставьте или замените - оставьте свой идентификатор

таким образом у вас может быть одна команда SQL

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

Изменить: добавлен вариант 2.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.