589

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

在 SQL Server 上插入更新存储过程

在 SQLite 中是否有一些我没有想到的聪明方法?

基本上,如果记录存在,我想更新四列中的三列,如果不存在,我想用第四列的默认(NUL)值插入记录。

ID 是主键,因此 UPSERT 永远只有一条记录。

(我试图避免 SELECT 的开销以确定我是否需要 UPDATE 或 INSERT 显然)

建议?


我无法在 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 
);

但前两个 blob 不会引起冲突,只有 ID 会所以我假设 Blob1 和 Blob2 不会被替换(根据需要)


当绑定数据是一个完整的事务时,SQLite 中的 UPDATE,这意味着要更新的每个发送的行都需要: Prepare/Bind/Step/Finalize 语句与允许使用重置功能的 INSERT 语句不同

语句对象的生命周期是这样的:

  1. 使用 sqlite3_prepare_v2() 创建对象
  2. 使用 sqlite3_bind_ 接口将值绑定到主机参数。
  3. 通过调用 sqlite3_step() 运行 SQL
  4. 使用 sqlite3_reset() 重置语句,然后返回步骤 2 并重复。
  5. 使用 sqlite3_finalize() 销毁语句对象。

UPDATE 我猜与 INSERT 相比速度很慢,但它与使用主键的 SELECT 相比如何?

也许我应该使用 select 来读取第 4 列(Blob3),然后使用 REPLACE 编写一条新记录,将原始第 4 列与前 3 列的新数据混合?

4

19 回答 19

915

假设表中有三列:ID、NAME、ROLE


坏:这将使用 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');

:在 SQLite 中使用 SQLite On 冲突子句 UPSERT 支持!UPSERT 语法已添加到 SQLite 版本 3.24.0!

UPSERT 是对 INSERT 的一种特殊语法,如果 INSERT 违反唯一性约束,它会导致 INSERT 表现为 UPDATE 或无操作。UPSERT 不是标准 SQL。SQLite 中的 UPSERT 遵循 PostgreSQL 建立的语法。

在此处输入图像描述

好但乏味:这将更新 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')
          );
于 2010-12-02T00:55:15.833 回答
144

插入或替换等同于“UPSERT”。

假设我有包含字段 id、name 和 role 的表 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 的预期输出是更改角色并保留名称。

于 2010-11-23T07:46:00.187 回答
117

如果您只想保留现有行中的一列或两列,Eric B 的回答是可以的。如果你想保留很多列,它会变得太麻烦。

这是一种可以很好地扩展到任意数量的列的方法。为了说明这一点,我将假设以下模式:

 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 将自动分配一个 ID,但如果已经存在这样的行,old.id将有一个实际值,这将被重用。这正是我想要的。

事实上,这是非常灵活的。请注意该ts列是如何在所有方面都完全缺失的——因为它有一个DEFAULT值,SQLite 在任何情况下都会做正确的事情,所以我不必自己处理它。

您还可以在newold两侧都包含一列,然后COALESCE(new.content, old.content)在外部使用例如SELECT“插入新内容(如果有),否则保留旧内容” - 例如,如果您使用固定查询并绑定新内容带有占位符的值。

于 2011-09-22T08:13:52.363 回答
88

如果您通常进行更新,我会..

  1. 开始交易
  2. 进行更新
  3. 检查行数
  4. 如果为 0 则插入
  5. 犯罪

如果您通常进行插入,我会

  1. 开始交易
  2. 尝试插入
  3. 检查主键违规错误
  4. 如果我们遇到错误,请进行更新
  5. 犯罪

这样你就可以避免选择,并且你在 Sqlite 上的事务是健全的。

于 2009-01-07T02:29:13.023 回答
86

此答案已更新,因此以下评论不再适用。

2018-05-18 停止新闻。

SQLite 中的 UPSERT 支持!UPSERT 语法已添加到 SQLite 版本 3.24.0(待定)!

UPSERT 是对 INSERT 的一种特殊语法,如果 INSERT 违反唯一性约束,它会导致 INSERT 表现为 UPDATE 或无操作。UPSERT 不是标准 SQL。SQLite 中的 UPSERT 遵循 PostgreSQL 建立的语法。

在此处输入图像描述

或者:

另一种完全不同的方法是:在我的应用程序中,当我在内存中创建行时,我将内存中的 rowID 设置为 long.MaxValue。(MaxValue 永远不会被用作 ID,您将活得不够长......然后,如果 rowID 不是那个值,那么它必须已经在数据库中,所以如果它是 MaxValue 则需要更新,那么它需要插入。仅当您可以跟踪应用程序中的 rowID 时,这才有用。

于 2015-08-28T12:00:13.723 回答
65

我意识到这是一个旧线程,但我最近一直在使用 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 子句,但似乎可以解决问题。我也有这样的想法,如果对 changes() 的调用大于零,sqlite 可以完全优化更新语句。它是否真的这样做超出了我的知识范围,但一个人可以做梦,不是吗?;)

对于奖励积分,您可以附加此行,它会返回该行的 id,无论它是新插入的行还是现有行。

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
于 2011-09-08T19:12:04.013 回答
18

从版本 3.24.0 开始,SQLite 支持 UPSERT。

文档中:

UPSERT 是对 INSERT 的一种特殊语法,如果 INSERT 违反唯一性约束,它会导致 INSERT 表现为 UPDATE 或无操作。UPSERT 不是标准 SQL。SQLite 中的 UPSERT 遵循 PostgreSQL 建立的语法。UPSERT 语法已添加到 SQLite 版本 3.24.0(待定)。

UPSERT 是一个普通的 INSERT 语句,后跟特殊的 ON CONFLICT 子句

在此处输入图像描述

图片来源:https ://www.sqlite.org/images/syntax/upsert-clause.gif


例子:

CREATE TABLE t1(id INT PRIMARY KEY, c TEXT);
INSERT INTO t1(id, c) VALUES (1,'a'), (2, 'b');
SELECT * FROM t1;


INSERT INTO t1(id, c) VALUES (1, 'c');
-- UNIQUE constraint failed: t1.id

INSERT INTO t1(id, c) VALUES (1, 'c')
ON CONFLICT DO NOTHING;

SELECT * FROM t1;

INSERT INTO t1(id, c)
VALUES (1, 'c')
ON CONFLICT(id) DO UPDATE SET c = excluded.c;

SELECT * FROM t1;

db<>小提琴演示

于 2018-05-11T17:25:20.690 回答
15

这是一个真正是 UPSERT(更新或插入)而不是插入或替换(在许多情况下工作方式不同)的解决方案。

它的工作原理如下:
1. 如果存在具有相同 Id 的记录,请尝试更新。
2. 如果更新没有改变任何行(NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)),则插入记录。

因此,要么更新现有记录,要么执行插入。

重要的细节是使用 changes() SQL 函数检查更新语句是否命中任何现有记录,如果没有命中任何记录,则仅执行插入语句。

值得一提的是,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;
于 2014-02-27T22:47:54.313 回答
13

伯恩哈特的更新:

你确实可以在 SQLite 中做一个 upsert,它只是看起来和你习惯的有点不同。它看起来像:

INSERT INTO table_name (id, column1, column2) 
VALUES ("youruuid", "value12", "value2")
ON CONFLICT(id) DO UPDATE 
SET column1 = "value1", column2 = "value2"
于 2018-07-12T13:01:25.883 回答
6

扩展亚里士多德的答案,您可以从一个虚拟的“单例”表(您自己创建的单行表)中选择。这避免了一些重复。

我还使示例在 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";
于 2013-05-21T02:07:41.337 回答
5

我知道的最好的方法是先更新,然后再插入。“选择的开销”是必要的,但这并不是一个可怕的负担,因为您正在搜索主键,这很快。

您应该能够使用您的表和字段名称修改以下语句以执行您想要的操作。

--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
                );
于 2009-01-07T02:01:10.773 回答
3

如果有人想阅读我在 Cordova 中的 SQLite 解决方案,由于上面的@david 回答,我得到了这个通用的 js 方法。

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”是我用作参考的 id,因为我正在与远程服务器同步。

SQLite Cordova插件的使用请参考官方链接

于 2017-01-31T15:57:00.883 回答
2

我认为这可能是您正在寻找的:ON CONFLICT 子句

如果你这样定义你的表:

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

现在,如果您使用已经存在的 id 执行 INSERT,SQLite 会自动执行 UPDATE 而不是 INSERT。

嗯...

于 2009-01-07T02:20:46.240 回答
2

此方法重新混合了此问题答案中的一些其他方法,并结合了 CTE(通用表表达式)的使用。我将介绍查询,然后解释我为什么这样做。

如果有员工 300,我想将员工 300 的姓氏更改为 DAVIS。否则,我将添加一个新员工。

表名:员工列: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 函数中将空值替换为应该使用的值来决定要使用的默认值。

于 2016-01-29T06:20:46.947 回答
2

遵循Aristotle Pagaltzis和Eric B's answer的想法COALESCE,这里是一个 upsert 选项,仅更新几列或插入整行(如果不存在)。

在这种情况下,假设应该更新标题和内容,在存在时保留其他旧值并在找不到名称时插入提供的值:

当它应该是自动增量时, NOTEid被强制为 NULL 。INSERT如果它只是一个生成的主键,那么COALESCE也可以使用(参见Aristotle Pagaltzis 评论)。

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

于 2017-12-30T20:08:14.510 回答
1

如果您不介意在两个操作中执行此操作。

脚步:

1)使用“插入或忽略”添加新项目

2) 使用“UPDATE”更新现有项目

这两个步骤的输入是相同的新项目或可更新项目的集合。适用于无需更改的现有项目。它们将被更新,但使用相同的数据,因此最终结果是没有变化。

当然,速度较慢等。效率低下。是的。

写sql容易维护和理解吗?确实。

这是一个需要考虑的权衡。非常适合小型 upserts。非常适合那些不介意为了代码可维护性而牺牲效率的人。

于 2020-01-05T23:49:32.700 回答
0

使用 WHERE 选择更新日期记录的完整示例。

-- https://www.db-fiddle.com/f/7jyj4n76MZHLLk2yszB6XD/22
 
DROP TABLE IF EXISTS db;

CREATE TABLE db
(
 id PRIMARY KEY,
 updated_at,
 other
);

-- initial INSERT
INSERT INTO db (id,updated_at,other) VALUES(1,1,1);

SELECT * FROM db;

-- INSERT without WHERE
INSERT INTO db (id,updated_at,other) VALUES(1,2,2)
ON CONFLICT(id) DO UPDATE SET updated_at=excluded.updated_at;

SELECT * FROM db;

-- WHERE is FALSE
INSERT INTO db (id,updated_at,other) VALUES(1,2,3)
ON CONFLICT(id) DO UPDATE SET updated_at=excluded.updated_at, other=excluded.other
WHERE excluded.updated_at > updated_at;

SELECT * FROM db;

-- ok to SET a PRIMARY KEY. WHERE is TRUE
INSERT INTO db (id,updated_at,other) VALUES(1,3,4)
ON CONFLICT(id) DO UPDATE SET id=excluded.id, updated_at=excluded.updated_at, other=excluded.other
WHERE excluded.updated_at > updated_at;

SELECT * FROM db;
于 2021-04-11T11:15:19.937 回答
-3

刚刚阅读了这个帖子,并对这个“UPSERT”并不容易感到失望,我进一步调查了......

实际上,您可以在 SQLITE 中直接轻松地执行此操作。

而不是使用:INSERT INTO

采用:INSERT OR REPLACE INTO

这正是你想要它做的!

于 2010-10-07T11:00:32.337 回答
-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;
于 2014-05-02T03:49:17.197 回答