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

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

вставить обновление, хранящееся в SQL Server

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

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

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

(Я пытаюсь избежать накладных расходов SELECT, чтобы определить, нужно ли мне обновлять или вставлять очевидно)

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


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

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

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

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


обновления в SQLite, когда данные привязки являются полной транзакцией, что означает Каждая отправленная строка для обновления требует: подготовка/привязка/шаг/завершение операторов в отличие от вставки, которая позволяет использовать функцию reset

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

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

UPDATE я предполагаю, медленно по сравнению с INSERT, но как он сравнивается с выбором с помощью первичного ключа?

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

17 ответов


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


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

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

плохое: это вставит или заменит 2 столбца... столбец NAME будет иметь значение NULL или значение по умолчанию:

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

хорошо: это обновит 2 столбца. Когда ID=1 существует, имя не будет изменено. Когда ID=1 не exist, имя будет по умолчанию (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')
          );

вставить или заменить -не эквивалент "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 будет изменение роли и сохранение имени.


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

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

 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 (очевидно!).

я достигаю истинного 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 value, SQLite просто сделает правильную вещь в любом случае, поэтому мне не нужно заботиться об этом самостоятельно.

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


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

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

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

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

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


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

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

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;

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

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

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

enter image description here

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

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

поэтому он пытается обновить, если запись есть, то вставка не является действием.

кроме того:

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


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

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

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

важная деталь-использовать changes () SQL функция для проверки, если инструкция update ударил любые существующие записи и только выполнить инструкцию 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;

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

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

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

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

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

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, '').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

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

"Values" - это массив, который вы должны построить раньше, и он представляет строки, которые вы хотите вставить или обновить в таблицу.

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

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


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

С документация:

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

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

enter image description here

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


вы действительно можете сделать 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

Я думаю, что это может быть то, что вы ищете: о предложении конфликта.

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

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

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

Hth...


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

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


после Аристотель 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


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

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

вместо: INSERT INTO

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

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


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;