SQLite - UPSERT * не * ВСТАВИТЬ или ЗАМЕНИТЬ

535

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 
);

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


ОБНОВЛЕНИЯ в SQLite, когда привязка данных является полной транзакцией, что означает, что каждая отправляемая строка, подлежащая обновлению, требует: операторов Prepare / Bind / Step / Finalize в отличие от 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 столбцов?

Майк Трейдер
источник
5
SQLite - UPSERT доступен в предварительном выпуске: sqlite.1065341.n5.nabble.com/…
gaurav
3
UPSERT доступен в версии 3.24.0 SQLite
pablo_worker

Ответы:

854

Предполагая три столбца в таблице: ID, NAME, ROLE


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

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

ПЛОХО: Это вставит или заменит 2 столбца ... столбцу ИМЯ будет присвоено значение NULL или значение по умолчанию:

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

ХОРОШО : Используйте SQLite On предложение о конфликте поддержки UPSERT в SQLite! Синтаксис UPSERT был добавлен в SQLite с версией 3.24.0!

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

введите описание изображения здесь

ХОРОШО, но старательно: это обновит 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')
          );
Эрик Б
источник
33
+1 гениально! Встроенное предложение select дает вам возможность переопределить стандартную функцию ON CONFLICT REPLACE, если вам нужно объединить / сравнить старое значение и новое значение для любого поля.
G__
24
Если на Сотрудника ссылаются другие строки с каскадным удалением, то другие строки все равно будут удалены путем замены.
Дон Реба
246
Это больно. SQlite нужен UPSERT.
andig
21
Не могли бы вы объяснить, почему это вставит или заменит все столбцы с новыми значениями для ID = 1: в первом примере это считается ПЛОХОЙ ? Представленная здесь команда предназначена для создания новой записи с идентификатором 1, именем Джона Фу и генеральным директором роли или перезаписать запись с идентификатором 1, если она уже существует, с этими данными (предполагается, что идентификатор является первичным ключом). , Итак, почему это плохо, если именно это происходит?
ИЛИ Mapper
10
@Cornelius: Это ясно, но это не то, что происходит в первом примере. Первый пример предназначен для принудительной установки всех столбцов, что и происходит, независимо от того, вставлена ​​или заменена запись. Итак, почему это считается плохим? Связанный ответ также только указывает, почему что-то плохое может произойти при указании подмножества столбцов, как во втором примере; похоже, он не раскрывает никаких негативных последствий того, что происходит в вашем первом примере, INSERT OR REPLACEа указывает значения для всех столбцов.
ИЛИ Mapper
134

ВСТАВКА ИЛИ ЗАМЕНА НЕ эквивалентны «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 будет состоять в том, чтобы изменить роль и сохранить имя.

gregschlom
источник
21
-1 от меня боюсь. Да, принятый ответ неверен, но, хотя ваш ответ указывает на проблему, он также не является ответом. Фактический ответ см. В умном решении Эрика Б. с использованием встроенного coalesce((select ..), 'new value')предложения. Я думаю, что для ответа Эрика здесь нужно больше голосов.
День
22
Верно. Эрик определенно лучший ответ и заслуживает большего количества голосов. При этом, я думаю, что, указав на проблему, я внес небольшой вклад в нахождение хорошего ответа (ответ Эрика пришел позже и основывается на примерах таблиц sql в моем ответе). Так что не уверен, если я заслуживаю -1, но не имеет значения :)
gregschlom
6
+1, чтобы сместить -1 выше. лол. Интересные сроки в этой теме, хотя. Очевидно, ваш ответ был за неделю до ответа Эрика, но вы оба ответили на вопрос почти через два года после того, как он был задан. +1 для Эрика тоже, хотя для уточнения.
Рич
2
@QED no, потому что delete + insert (который является заменой) - это 2 оператора dml, например, со своими собственными триггерами. Это не то же самое, что 1 заявление об обновлении.
Себас
109

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

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

 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сказать «вставить новое содержание, не было ли, в противном случае сохранить старое содержание» , - например , если вы используете фиксированный запрос и являются обязательными для нового значения с заполнителями.

Аристотель Пагальцис
источник
13
+1, прекрасно работает, но добавьте WHERE name = "about"ограничение на SELECT ... AS oldускорение. Если у вас 1м + рядов, это очень медленно.
Хороший вопрос, +1 на ваш комментарий. Я оставлю это в стороне от ответа, потому что добавление такого WHEREпредложения требует именно такой избыточности в запросе, которую я пытался избежать в первую очередь, когда придумал этот подход. Как всегда: когда вам нужна производительность, денормализуйте - структуру запроса, в данном случае.
Аристотель Пагальцис
4
Вы можете упростить пример Аристотеля до этого, если хотите: INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT id, 'about', 'About this site', content, 42 FROM ( SELECT NULL ) LEFT JOIN ( SELECT * FROM page WHERE name = 'about' )
jcox
4
Не излишне ли это ON DELETEзапускает триггеры, когда он выполняет замену (то есть обновление)?
Каракури
3
Это, безусловно, вызовет ON DELETEтриггеры. Незнаю о излишне. Для большинства пользователей это, вероятно, будет ненужным, даже нежелательным, но, возможно, не для всех пользователей. Аналогичным образом за то, что он также будет каскадно удалять любые строки с внешними ключами в рассматриваемой строке - вероятно, это проблема для многих пользователей. К сожалению, SQLite не имеет ничего общего с настоящим UPSERT. ( INSTEAD OF UPDATEЯ полагаю, за исключением подделки его спусковым крючком.)
Аристотель Пагальцис
86

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

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

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

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

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

Сэм Шафран
источник
Спасибо, только что спросил это @ stackoverflow.com/questions/2717590/… . +1 от меня! =)
Аликс Аксель
4
Если вы собираетесь проверить количество строк с помощью sqlite3_changes () на 3-м шаге, убедитесь, что вы не используете дескриптор БД из нескольких потоков для изменений.
Линулин
3
Не будет ли следующее менее многословным, но с тем же эффектом: 1) выберите таблицу формы идентификатора, где id = 'x' 2) if (ResultSet.rows.length == 0) обновите таблицу, где id = 'x';
Флорин
20
Мне бы очень хотелось, чтобы INSERT OR UPDATE были частью языка
Florin
В конечном итоге это оказалось лучшим для меня, пытаясь выполнить REPLACE INTO или всегда, а вставка / обновление убивали мою производительность, когда я в основном делал обновления вместо вставок.
PherricOxide
83

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

2018-05-18 СТОП ПРЕСС.

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

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

введите описание изображения здесь

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

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

AnthonyLambert
источник
5
Аминь. Простое лучше, чем сложное. Это немного проще, чем принятый ответ.
kkurian
4
Я думал, что вы не можете сделать INSERT INTO ... ГДЕ в sqlite? Это синтаксическая ошибка для меня в sqlite3
Чарли Мартин
5
@CharlieMartin правильно. Этот синтаксис недопустим для SQLite - это то, что запросил OP. Предложение WHEREне может быть добавлено к INSERTутверждению: sqlite-insert ...
colm.anseo
4
Этот ответ заставил меня потерять много времени, вопрос касается SQLITE, и я не знал, что INSERT WHERE не был поддержан в sqite, пожалуйста, добавьте это замечание, что он недействителен для sqlite в вашем ответе
Miquel
3
@colminator и другие: я исправил его оператор SQL, используя ваше предложение.
Ф Лекхас
62

Я понимаю, что это старый поток, но в последнее время я работал в 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;
Крис Ставропулос
источник
+ 1. Именно то, что я пытался заменить, делая преобразование из некоторого кода SQL Server TSQL. Спасибо. SQL, который я пытался заменить, был похожUpdate <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
DarrenMB
14

Вот решение, которое действительно является UPSERT (UPDATE или INSERT) вместо INSERT OR REPLACE (которое во многих ситуациях работает по-разному).

Это работает так:
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;
Дэвид Либехерр
источник
2
Кажется, это лучшее решение для меня, чем Эрик. Однако INSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE changes() = 0;должно также работать.
bkausbk
Спасибо, парень, он также работает в WebSQL (используя плагин Cordova и SQLite)
Zappescu,
11

Начиная с версии 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.

введите описание изображения здесь

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

Лукаш Шозда
источник
8

Вы действительно можете сделать переход в 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
Брилл Паппин
источник
5

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

Я также сохранил пример переносимого через 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";
Сообщество
источник
4

Лучший подход, который я знаю, - это сделать обновление с последующей вставкой. «Затраты на выбор» необходимы, но это не страшное бремя, поскольку вы выполняете поиск по первичному ключу, что очень быстро.

Вы должны иметь возможность изменять приведенные ниже операторы с именами таблиц и полей, чтобы делать то, что вы хотите.

--first, update any matches
UPDATE DESTINATION_TABLE DT
SET
  MY_FIELD1 = (
              SELECT MY_FIELD1
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
 ,MY_FIELD2 = (
              SELECT MY_FIELD2
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
WHERE EXISTS(
            SELECT ST2.PRIMARY_KEY
            FROM
              SOURCE_TABLE ST2
             ,DESTINATION_TABLE DT2
            WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY
            );

--second, insert any non-matches
INSERT INTO DESTINATION_TABLE(
  MY_FIELD1
 ,MY_FIELD2
)
SELECT
  ST.MY_FIELD1
 ,NULL AS MY_FIELD2  --insert NULL into this field
FROM
  SOURCE_TABLE ST
WHERE NOT EXISTS(
                SELECT DT2.PRIMARY_KEY
                FROM DESTINATION_TABLE DT2
                WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY
                );
JosephStyons
источник
7
Способ сложный.
2010 года
Я думаю, что это не очень хорошая идея, потому что вам нужно дважды выполнить запрос к базе данных.
Рикардо да Роша Витор
3

Если кто-то хочет прочитать мое решение для SQLite в Кордове, я получил этот общий метод js благодаря ответу @david выше.

function    addOrUpdateRecords(tableName, values, callback) {
get_columnNames(tableName, function (data) {
    var columnNames = data;
    myDb.transaction(function (transaction) {
        var query_update = "";
        var query_insert = "";
        var update_string = "UPDATE " + tableName + " SET ";
        var insert_string = "INSERT INTO " + tableName + " SELECT ";
        myDb.transaction(function (transaction) {
            // Data from the array [[data1, ... datan],[()],[()]...]:
            $.each(values, function (index1, value1) {
                var sel_str = "";
                var upd_str = "";
                var remoteid = "";
                $.each(value1, function (index2, value2) {
                    if (index2 == 0) remoteid = value2;
                    upd_str = upd_str + columnNames[index2] + "='" + value2 + "', ";
                    sel_str = sel_str + "'" + value2 + "', ";
                });
                sel_str = sel_str.substr(0, sel_str.length - 2);
                sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);";
                upd_str = upd_str.substr(0, upd_str.length - 2);
                upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';";                    
                query_update = update_string + upd_str;
                query_insert = insert_string + sel_str;  
                // Start transaction:
                transaction.executeSql(query_update);
                transaction.executeSql(query_insert);                    
            });
        }, function (error) {
            callback("Error: " + error);
        }, function () {
            callback("Success");
        });
    });
});
}

Итак, сначала подберите имена столбцов с помощью этой функции:

function get_columnNames(tableName, callback) {
myDb.transaction(function (transaction) {
    var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'";
    transaction.executeSql(query_exec, [], function (tx, results) {
        var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

Затем создайте транзакции программно.

«Значения» - это массив, который вы должны построить раньше, и он представляет строки, которые вы хотите вставить или обновить в таблице.

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

Для использования плагина SQLite Cordova, пожалуйста, обратитесь к официальной ссылке

Zappescu
источник
2

Этот метод смешивает несколько других методов из ответа на этот вопрос и включает в себя использование CTE (Common Table Expressions). Я представлю запрос, а затем объясню, почему я сделал то, что сделал.

Я хотел бы изменить фамилию сотрудника 300 на ДЭВИС, если есть сотрудник 300. В противном случае я добавлю нового сотрудника.

Имя таблицы: сотрудники Столбцы: id, first_name, last_name

Запрос:

INSERT OR REPLACE INTO employees (employee_id, first_name, last_name)
WITH registered_employees AS ( --CTE for checking if the row exists or not
    SELECT --this is needed to ensure that the null row comes second
        *
    FROM (
        SELECT --an existing row
            *
        FROM
            employees
        WHERE
            employee_id = '300'

        UNION

        SELECT --a dummy row if the original cannot be found
            NULL AS employee_id,
            NULL AS first_name,
            NULL AS last_name
    )
    ORDER BY
        employee_id IS NULL --we want nulls to be last
    LIMIT 1 --we only want one row from this statement
)
SELECT --this is where you provide defaults for what you would like to insert
    registered_employees.employee_id, --if this is null the SQLite default will be used
    COALESCE(registered_employees.first_name, 'SALLY'),
    'DAVIS'
FROM
    registered_employees
;

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

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

Додзи Дзакума
источник
2

Вслед за Аристотель Pagaltzis и идею COALESCEот ответа Эрика Б , вот это вариант upsert обновить только несколько столбцов или вставить полную строку , если она не существует.

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

ПРИМЕЧАНИЕ id принудительно принимает значение NULL, когда INSERTпредполагается автоинкремент. Если это просто сгенерированный первичный ключ, COALESCEего также можно использовать (см. Комментарий Аристотеля Пагальтзиса ).

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

Таким образом, общее правило: если вы хотите сохранить старые значения, используйте COALESCE, когда вы хотите обновить значения, используйтеnew.fieldname

Микель
источник
COALESCE(old.id, new.id)определенно не так с автоинкрементным ключом. И хотя «держать большую часть строки без изменений, за исключением случаев, когда значения отсутствуют» звучит как сценарий использования, который кто-то может иметь на самом деле, я не думаю, что это то, что люди ищут, когда ищут, как сделать UPSERT.
Аристотель Пагальцис
@AristotlePagaltzis прошу прощения, если я ошибаюсь, я не использую автоинкременты. Что я ищу с этим запросом, чтобы обновить только несколько столбцов или вставить полную строку с предоставленными значениями, если он не существует . Я играл с вашим запросом и не смог достичь его при вставке: столбцы, выбранные из oldтаблицы, в которой они были назначены NULL, а не значения, указанные в new. Это причина для использования COALESCE. Я не эксперт в sqlite, я тестировал этот запрос и, кажется, работает для этого случая, я был бы очень признателен, если бы вы могли указать мне на решение с автоинкрементами
Miquel
2
В случае автоинкрементного ключа вы хотите вставить его NULLкак ключ, потому что это говорит SQLite вместо этого вставить следующее доступное значение.
Аристотель Пагальцис
Я не говорю, что вам не следует делать то, что вы делаете (если вам это нужно, тогда вам это нужно), просто это не совсем ответ на этот вопрос. Обычно UPSERT означает, что у вас есть строка, которую вы хотите сохранить в таблице, и вы просто не знаете, есть ли у вас подходящая строка для ввода значений или вам нужно вставить их как новую строку. Ваш вариант использования таков: если у вас уже есть подходящая строка, вы хотите игнорировать большинство значений из новой строки . Это нормально, это не тот вопрос, который был задан.
Аристотель Пагальцис
1

Я думаю, что это может быть то, что вы ищете: ON CONFLICT .

Если вы определите свою таблицу следующим образом:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    field1 TEXT 
); 

Теперь, если вы делаете INSERT с уже существующим идентификатором, SQLite автоматически выполняет UPDATE вместо INSERT.

Hth ...

kmelvn
источник
6
Я не думаю, что это работает, это уничтожит столбцы, отсутствующие в операторе вставки
Сэм Саффрон
3
@Mosor: -1 от меня, извините. Это то же самое, что и REPLACEвыписка.
Аликс Аксель
7
-1, потому что это делает удаление, а затем вставку, если первичный ключ уже существует.
2010 г.
1

Если вы не против сделать это в две операции.

шаги:

1) Добавьте новые предметы с помощью «INSERT OR IGNORE»

2) Обновить существующие элементы с помощью «ОБНОВЛЕНИЕ»

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

Конечно, медленнее и т. Д. Неэффективно. Ага.

Легко написать SQL и поддерживать и понимать это? Определенно.

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

sapbucket
источник
Примечание для всех: ВСТАВИТЬ ИЛИ ЗАМЕНИТЬ НЕ то, о чем этот пост. INSERT OR REPLACE создаст новую строку в вашей таблице с новым идентификатором. Это не ОБНОВЛЕНИЕ.
сапбакет
-2

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

Вы можете сделать это прямо и легко в SQLITE.

Вместо использования: INSERT INTO

Использование: INSERT OR REPLACE INTO

Это именно то, что вы хотите!

SBB
источник
21
-1 INSERT OR REPLACEне является UPSERT. Смотри "ответ" Грегшлома по причине почему. Решение Эрика Б. на самом деле работает и нуждается в некоторых ответах.
День
-4
SELECT COUNT(*) FROM table1 WHERE id = 1;

если COUNT(*) = 0

INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3);

иначе если COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;
MJB
источник
Это слишком сложно, SQL может прекрасно справиться с этим в одном запросе
Робин Кантерс