SQLite - UPSERT * не * Вставить или заменить

408

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

Вставить обновление хранимой процедуры на SQL Server

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

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

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

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

Предложения?

  • 4
    SQLite - UPSERT, доступный в предварительной версии, см .: sqlite.1065341.n5.nabble.com/…
  • 2
    UPSERT доступен в версии 3.24.0 SQLite
Теги:
upsert

15 ответов

708
Лучший ответ

Предположим, что в таблице 3 столбца. ИД, ИМЯ, РОЛЬ


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 существует, NAME не будет затронут. Когда 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 существует, ROLE не будет затронут. Когда ID = 1 не существует, роль будет установлена ​​на "Benchwarmer" вместо значения по умолчанию.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
  • 30
    +1 гениально! Встроенное предложение select дает вам возможность переопределить стандартную функцию ON CONFLICT REPLACE, если вам нужно объединить / сравнить старое значение и новое значение для любого поля.
  • 22
    Если на Сотрудника ссылаются другие строки с каскадным удалением, то другие строки все равно будут удалены путем замены.
Показать ещё 14 комментариев
104

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

Скажем, у меня есть таблица Employee с именами полей, именем и ролью:

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 будет состоять в том, чтобы изменить роль и сохранить имя.

  • 18
    -1 от меня боюсь. Да, принятый ответ неверен, но, хотя ваш ответ указывает на проблему, он также не является ответом. Фактический ответ см. В умном решении Эрика Б. с использованием встроенного предложения coalesce((select ..), 'new value') . Я думаю, что для ответа Эрика здесь нужно больше голосов.
  • 20
    В самом деле. Эрик определенно лучший ответ и заслуживает большего количества голосов. При этом, я думаю, что, указав на проблему, я внес небольшой вклад в нахождение хорошего ответа (ответ Эрика пришел позже и основывается на примерах таблиц sql в моем ответе). Так что не уверен, если я заслуживаю -1, но не важно :)
Показать ещё 5 комментариев
88

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

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

 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 нужно выбрать значение ID при вставке новой строки. Но при обновлении существующей строки на основе ее name я хочу, чтобы она продолжала иметь старое значение ID (очевидно!).

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

  • 11
    +1, прекрасно работает, но добавьте ограничение WHERE name = "about" в SELECT ... AS old чтобы ускорить процесс. Если у вас 1м + рядов, это очень медленно.
  • 0
    Хороший вопрос, +1 на ваш комментарий. Я оставлю это без ответа, потому что добавление такого предложения WHERE требует только той избыточности в запросе, которую я пытался избежать в первую очередь, когда придумал этот подход. Как всегда: когда вам нужна производительность, денормализуйте - структуру запроса, в данном случае.
Показать ещё 9 комментариев
68

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

  • Начать транзакцию
  • Сделайте обновление
  • Проверьте число строк
  • Если это 0, вставьте
  • Commit

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

  • Начать транзакцию
  • Попробуйте вставить
  • Проверьте ошибку нарушения первичного ключа.
  • Если мы получили сообщение об ошибке, выполните обновление
  • Commit

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

  • 0
    Спасибо, только что спросил это @ stackoverflow.com/questions/2717590/… . +1 от меня! знак равно
  • 3
    Если вы собираетесь проверить количество строк с помощью sqlite3_changes () на третьем шаге, убедитесь, что вы не используете дескриптор БД из нескольких потоков для изменений.
Показать ещё 5 комментариев
54

Я понимаю, что это старый поток, но я уже давно работал в 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 может полностью оптимизировать оператор обновления, если вызов изменений() больше нуля. Является ли это на самом деле тем, что мне известно, но человек может мечтать, не так ли?;)

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

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
  • 0
    + 1. Именно то, что я пытался заменить, делая преобразование из некоторого кода SQL Server TSQL. Благодарю. SQL, который я пытался заменить, был похож на Update <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
36

Я знаю, что опаздываю на вечеринку, но....

UPDATE employee SET role = 'code_monkey', name='fred' WHERE id = 1;
INSERT INTO employee(id, role, name) values (1, 'code monkey, 'fred') WHERE changes() = 0;

Таким образом, он пытается обновить, если запись есть там, тогда изменяется() == 1, поэтому вставка не изменяется.

в качестве альтернативы:

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

  • 4
    Аминь. Простое лучше, чем сложное. Это немного проще, чем принятый ответ.
  • 5
    Примерно через 60 лет мой ответ может выйти в лидеры ....
Показать ещё 14 комментариев
12

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

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

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

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

Следует отметить, что функция 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; также должен работать.
  • 0
    Спасибо человек, это также работает в WebSQL (используя плагин Cordova и SQLite)
5

Развернув ответ Аристотеля, вы можете выбрать из таблицы фиктивных "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";
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 обратитесь к официальной ссылке

3

Мосор,

Я не могу подтвердить, что синтаксис на сайте 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, который позволяет использовать функцию reset

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

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

UPDATE Я предполагаю, что медленнее по сравнению с INSERT, но как он сравнивается с SELECT с использованием основного ключа?

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

3

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

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

--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
                );
  • 7
    Способ сложный.
  • 0
    Я думаю, что это не очень хорошая идея, потому что вам нужно дважды выполнить запрос к базе данных.
1

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

Я хотел бы изменить фамилию сотрудника 300 на DAVIS, если есть сотрудник 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, какими должны быть значения.

1

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

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

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

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

Hth...

  • 6
    Я не думаю, что это работает, это уничтожит столбцы, отсутствующие в операторе вставки
  • 2
    @Mosor: -1 от меня, извините. Это то же самое, что и выписка оператора REPLACE .
Показать ещё 1 комментарий
-1

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

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

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

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

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

  • 21
    -1 INSERT OR REPLACE - это НЕ UPSERT . Смотри "ответ" Грегшлома по той причине, почему. Решение Эрика Б. на самом деле работает и нуждается в некоторых ответах.
  • 0
    -1 Однозначно правильный ответ - ответ Эрика Б!
Показать ещё 1 комментарий
-3
SELECT COUNT(*) FROM table1 WHERE id = 1;

если COUNT(*) = 0

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

else if COUNT(*) > 0

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню