SQLite - UPSERT * не * Вставить или заменить - PullRequest
494 голосов
/ 07 января 2009

http://en.wikipedia.org/wiki/Upsert

Вставить обновление сохраненного процесса на SQL Server

Есть ли какой-нибудь умный способ сделать это в SQLite, о котором я не думал?

В основном я хочу обновить три из четырех столбцов, если запись существует, Если он не существует, я хочу вставить запись со значением по умолчанию (NUL) для четвертого столбца.

Идентификатор является первичным ключом, поэтому в UPSERT будет только одна запись.

(я пытаюсь избежать издержек SELECT, чтобы определить, нужно ли мне ОБНОВИТЬ или ВСТАВИТЬ, очевидно)

Предложения


Я не могу подтвердить, что синтаксис на сайте SQLite для TABLE CREATE. Я не создал демо для его тестирования, но, похоже, он не поддерживается ..

Если бы это было так, у меня есть три столбца, поэтому на самом деле это выглядело бы так:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    Blob1 BLOB ON CONFLICT REPLACE, 
    Blob2 BLOB ON CONFLICT REPLACE, 
    Blob3 BLOB 
);

но первые два двоичных объекта не вызовут конфликта, только идентификатор Так что я asusme Blob1 и Blob2 не будет заменен (по желанию)


ОБНОВЛЕНИЯ в SQLite, когда привязка данных является полной транзакцией, что означает Каждая отправляемая строка, подлежащая обновлению, требует: Подготовить / Связать / Шаг / Завершить операторы в отличие от INSERT, который позволяет использовать функцию сброса

Жизнь объекта оператора выглядит примерно так:

  1. Создать объект с помощью sqlite3_prepare_v2 ()
  2. Привязка значений к параметрам хоста с использованием интерфейсов sqlite3_bind_.
  3. Запустите SQL, вызвав sqlite3_step ()
  4. Сбросьте инструкцию с помощью sqlite3_reset (), затем вернитесь к шагу 2 и повторите.
  5. Уничтожить объект оператора с помощью sqlite3_finalize ().

ОБНОВЛЕНИЕ Я предполагаю, что медленнее по сравнению с INSERT, но как это сравнить с SELECT с использованием первичного ключа?

Возможно, мне следует использовать select для чтения 4-го столбца (Blob3), а затем использовать REPLACE для записи новой записи, смешивая исходный 4-й столбец с новыми данными для первых 3 столбцов?

Ответы [ 17 ]

810 голосов
/ 02 декабря 2010

Предполагая 3 столбца в таблице. ID, ИМЯ, РОЛЬ


BAD: Это вставит или заменит все столбцы с новыми значениями для ID = 1:

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (1, 'John Foo', 'CEO');

BAD: Это вставит или заменит 2 столбца ... для столбца NAME будет установлено значение NULL или значение по умолчанию:

INSERT OR REPLACE INTO Employee (id, role) 
  VALUES (1, 'code monkey');

ХОРОШО: Это обновит 2 столбца. Когда ID = 1 существует, имя не изменится. Когда ID = 1 не существует, имя будет по умолчанию (NULL).

INSERT OR REPLACE INTO Employee (id, role, name) 
  VALUES (  1, 
            'code monkey',
            (SELECT name FROM Employee WHERE id = 1)
          );

Это обновит 2 столбца. Когда ID = 1 существует, роль не будет затронута. Когда ID = 1 не существует, роль будет установлена ​​на «Benchwarmer» вместо значения по умолчанию.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
125 голосов
/ 23 ноября 2010

ВСТАВИТЬ ИЛИ ЗАМЕНИТЬ НЕ эквивалентно "UPSERT".

Допустим, у меня есть таблица Employee с полями id, name и role:

INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO")
INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey")

Бум, вы потеряли имя сотрудника № 1. SQLite заменил его значением по умолчанию.

Ожидаемый вывод UPSERT - изменить роль и сохранить имя.

106 голосов
/ 22 сентября 2011

Ответ Эрика Б в порядке, если вы хотите сохранить один или два столбца из существующей строки. Если вы хотите сохранить много столбцов, он становится слишком громоздким.

Вот подход, который хорошо масштабируется для любого количества столбцов с обеих сторон. Для иллюстрации приведу следующую схему:

 CREATE TABLE page (
     id      INTEGER PRIMARY KEY,
     name    TEXT UNIQUE,
     title   TEXT,
     content TEXT,
     author  INTEGER NOT NULL REFERENCES user (id),
     ts      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );

Обратите внимание, в частности, что name является естественным ключом строки - id используется только для внешних ключей, поэтому SQLite сам выбирает значение идентификатора при вставке новой строки. Но при обновлении существующей строки на основе ее name я хочу, чтобы она продолжала иметь старое значение идентификатора (очевидно!).

Я достиг истинного UPSERT с помощью следующей конструкции:

 WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) )
 INSERT OR REPLACE INTO page (id, name, title, content, author)
 SELECT old.id, new.name, new.title, old.content, new.author
 FROM new LEFT JOIN page AS old ON new.name = old.name;

Точная форма этого запроса может немного отличаться. Ключом является использование INSERT SELECT с левым внешним соединением для присоединения существующей строки к новым значениям.

Здесь, если строка ранее не существовала, old.id будет NULL, а затем SQLite автоматически назначит идентификатор, но если такая строка уже была, old.id будет иметь фактическое значение, и это будет быть повторно использованы. Что именно то, что я хотел.

На самом деле это очень гибко. Обратите внимание, что столбец ts полностью отсутствует со всех сторон - поскольку он имеет значение DEFAULT, SQLite в любом случае будет действовать правильно, поэтому мне не придется самому об этом заботиться.

Вы также можете включить столбец с обеих сторон new и old, а затем использовать, например, COALESCE(new.content, old.content) во внешнем SELECT, чтобы сказать «вставьте новый контент, если таковой был, иначе сохраните старый контент» - например, если вы используете фиксированный запрос и связываете новые значения с заполнителями.

79 голосов
/ 07 января 2009

Если вы обычно делаете обновления, я бы ..

  1. Начать транзакцию
  2. сделать обновление
  3. Проверьте количество строк
  4. Если это 0, вставьте
  5. Совершить

Если вы обычно делаете вставки, я бы

  1. Начать транзакцию
  2. Попробуйте вставить
  3. Проверка ошибки нарушения первичного ключа
  4. если мы получили ошибку, сделайте обновление
  5. Совершить

Таким образом, вы избегаете выбора и получаете транзакционный звук на Sqlite.

68 голосов
/ 28 августа 2015

Этот ответ был обновлен, поэтому приведенные ниже комментарии больше не применяются.

2018-05-18 STOP PRESS.

Поддержка UPSERT в SQLite! Синтаксис UPSERT был добавлен в SQLite с версией 3.24.0 (ожидается)!

UPSERT - это специальное синтаксическое дополнение к INSERT, которое заставляет INSERT вести себя как UPDATE или no-op, если INSERT нарушает ограничение уникальности. UPSERT не является стандартным SQL. UPSERT в SQLite следует синтаксису, установленному PostgreSQL.

enter image description here

альтернативно:

Еще один совершенно другой способ сделать это: в моем приложении я установил свой идентификатор строки в памяти как long.MaxValue, когда я создаю строку в памяти. (MaxValue никогда не будет использоваться в качестве идентификатора, который вы не проживете достаточно долго ... Тогда, если rowID не является этим значением, тогда он уже должен быть в базе данных, поэтому требуется ОБНОВЛЕНИЕ, если это MaxValue, тогда ему нужна вставка. Это полезно, только если вы можете отслеживать идентификаторы строк в своем приложении.

59 голосов
/ 08 сентября 2011

Я понимаю, что это старый поток, но в последнее время я работал в sqlite3 и придумал этот метод, который лучше соответствовал моим потребностям в динамическом создании параметризованных запросов:

insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); 
update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

Это все еще 2 запроса с предложением where при обновлении, но, похоже, это помогает. У меня также есть такое видение, что sqlite может полностью оптимизировать оператор обновления, если вызов change () больше нуля. Насколько мне известно, действительно ли это на самом деле, но человек может мечтать, не так ли? ;)

Для бонусных баллов вы можете добавить эту строку, которая возвращает вам идентификатор строки, будь то новая вставленная строка или существующая строка.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
13 голосов
/ 28 февраля 2014

Вот решение, которое на самом деле представляет собой UPSERT (ОБНОВЛЕНИЕ или ВСТАВКА) вместо ВСТАВКИ ИЛИ ЗАМЕНЫ (которая работает по-разному во многих ситуациях).

Работает так:
1. Попробуйте обновить, если существует запись с таким же Id.
2. Если обновление не изменило ни одной строки (NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)), вставьте запись.

Таким образом, либо существующая запись была обновлена, либо будет выполнена вставка.

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

Следует отметить, что функция changes () не возвращает изменений, выполненных триггерами более низкого уровня (см. http://sqlite.org/lang_corefunc.html#changes),, поэтому обязательно примите это во внимание.

Вот SQL ...

Тестовое обновление:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 2;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 2, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

Тестовая вставка:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 3;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 3, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;
5 голосов
/ 21 мая 2013

Расширяя на Ответ Аристотеля , вы можете ВЫБРАТЬ из фиктивной таблицы "singleton" (таблица вашего собственного создания с одной строкой) Это позволяет избежать некоторого дублирования.

Я также сохранил пример переносимого через MySQL и SQLite и использовал столбец date_added в качестве примера того, как вы можете установить столбец только в первый раз.

 REPLACE INTO page (
   id,
   name,
   title,
   content,
   author,
   date_added)
 SELECT
   old.id,
   "about",
   "About this site",
   old.content,
   42,
   IFNULL(old.date_added,"21/05/2013")
 FROM singleton
 LEFT JOIN page AS old ON old.name = "about";
5 голосов
/ 12 июля 2018

Вы действительно можете сделать upsert в SQLite, он выглядит немного иначе, чем вы привыкли. Это будет выглядеть примерно так:

INSERT INTO table name (column1, column2) 
VALUES ("value12", "value2") WHERE id = 123 
ON CONFLICT DO UPDATE 
SET column1 = "value1", column2 = "value2" WHERE id = 123
4 голосов
/ 11 мая 2018

Начиная с версии 3.24.0, UPSERT поддерживается SQLite.

Из документации :

UPSERT - это специальное синтаксическое дополнение к INSERT, которое заставляет INSERT вести себя как UPDATE или no-op, если INSERT нарушает ограничение уникальности. UPSERT не является стандартным SQL. UPSERT в SQLite следует синтаксису, установленному PostgreSQL. Синтаксис UPSERT был добавлен в SQLite с версией 3.24.0 (ожидается).

UPSERT - это обычный оператор INSERT, за которым следует специальное предложение ON CONFLICT

enter image description here

Источник изображения: https://www.sqlite.org/images/syntax/upsert-clause.gif

...