SQLite UPSERT / 更新或插入

Posted

技术标签:

【中文标题】SQLite UPSERT / 更新或插入【英文标题】:SQLite UPSERT / UPDATE OR INSERT 【发布时间】:2013-02-23 00:23:30 【问题描述】:

我需要对 SQLite 数据库执行 UPSERT / INSERT OR UPDATE。

命令 INSERT OR REPLACE 在很多情况下很有用。但是,如果您想因为外键而使您的 id 保持在适当的位置,则它不起作用,因为它会删除该行,创建一个新行,因此该新行具有一个新 ID。

这是一张桌子:

玩家 -(id 上的主键,用户名唯一)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |

【问题讨论】:

【参考方案1】:

问答风格

好吧,在研究和解决这个问题几个小时后,我发现有两种方法可以实现这一点,具体取决于表的结构以及是否激活了外键限制以保持完整性。我想以简洁的格式分享此内容,以便为可能遇到我这种情况的人节省一些时间。

选项 1:您可以负担得起删除行

换句话说,您没有外键,或者如果您有外键,您的 SQLite 引擎已配置为不存在完整性异常。要走的路是插入或替换。如果您尝试插入/更新 ID 已存在的播放器,SQLite 引擎将删除该行并插入您提供的数据。现在问题来了:如何保持旧 ID 关联?

假设我们想要 UPSERT 使用数据 user_name='steven' 和 age=32。

看这段代码:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

诀窍在于合并。它返回用户 'steven' 的 id(如果有),否则返回一个新的新 id。

选项 2:您不能删除该行

在尝试了之前的解决方案后,我意识到在我的情况下,这可能最终会破坏数据,因为此 ID 用作其他表的外键。此外,我使用子句 ON DELETE CASCADE 创建了表,这意味着它会静默删除数据。危险。

所以,我首先想到了一个IF子句,但是SQLite只有CASE。如果 EXISTS(select id from player where user_name='steven' ),如果没有,则 INSERT。不行。

然后,最后我使用了蛮力,成功了。逻辑是,对于您要执行的每个 UPSERT,首先执行一个 INSERT OR IGNORE 以确保我们的用户有一行,然后执行一个 UPDATE 查询与您尝试插入的数据完全相同。

与以前相同的数据:user_name='steven' 和 age=32。

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

仅此而已!

编辑

正如 Andy 所评论的,尝试先插入然后更新可能会导致触发触发器的频率高于预期。在我看来,这不是数据安全问题,但确实触发不必要的事件毫无意义。因此,改进的解决方案是:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

【讨论】:

同上...选项 2 很棒。除了,我做了相反的事情:尝试更新,检查 rowsAffected > 0,如果没有,则进行插入。 这也是一个很好的方法,唯一的小缺点是你没有只有一个 SQL 用于“upsert”。 您不需要在最后一个代码示例的更新语句中重新设置用户名。设置年龄就够了。【参考方案2】:

这是一个迟到的答案。从 2018 年 6 月 4 日发布的 SQLite 3.24.0 开始,终于支持遵循 PostgreSQL 语法的UPSERT 子句。

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

注意:对于那些必须使用早于 3.24.0 版本的 SQLite,请参考下面的this answer(由我发布,@MarqueIV)。

但是,如果您确实可以选择升级,强烈建议您这样做,因为与我的解决方案不同,此处发布的解决方案在单个语句中实现了所需的行为。此外,您还可以获得更新版本通常附带的所有其他功能、改进和错误修复。

【讨论】:

目前,Ubuntu 存储库中还没有此版本。 为什么我不能在安卓上使用这个?我试过db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?")。给我一个关于单词“on”的语法错误 @BastianVoigt 因为安装在各种 android 版本上的 SQLite3 库都早于 3.24.0。请参阅:developer.android.com/reference/android/database/sqlite/… 遗憾的是,如果您需要在 Android 或 ios 上使用 SQLite3(或任何其他系统库)的新功能,您需要在应用程序中捆绑特定版本的 SQLite,而不是依赖安装的系统。跨度> 而不是UPSERT,这不是更多的INDATE,因为它首先尝试插入? ;) @BastianVoigt,请在下面查看我的答案(链接在上面的问题中),该答案适用于 3.24.0 之前的版本。【参考方案3】:

这是一种不需要蛮力“忽略”的方法,该方法仅在存在密钥违规时才有效。这种方式基于您在更新中指定的任何条件工作。

试试这个...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

工作原理

这里的“魔法酱”是在Where 子句中使用Changes()Changes() 表示上次操作影响的行数,在本例中为更新。

在上面的示例中,如果更新没有更改(即记录不存在),则Changes() = 0 因此Insert 语句中的Where 子句的计算结果为true,并且新行插入指定的数据。

如果Update 确实 更新了现有行,则Changes() = 1(或更准确地说,如果更新了不止一行,则不为零),因此 'Where' 子句Insert 现在评估为 false,因此不会发生插入。

这样做的好处是不需要蛮力,也不需要删除,然后重新插入可能导致外键关系中下游键混乱的数据。

此外,由于它只是一个标准的Where 子句,它可以基于您定义的任何内容,而不仅仅是键违规。同样,您可以将Changes() 与您想要/需要的任何其他内容结合使用,只要允许使用表达式即可。

【讨论】:

这对我很有用。除了所有 INSERT OR REPLACE 示例之外,我还没有在其他任何地方看到此解决方案,它对我的​​用例来说更加灵活。 @MarqueIV 如果有两个项目必须更新或插入怎么办?例如,第一个已更新,而第二个不存在。在这种情况下Changes() = 0 将返回 false 并且两行将执行 INSERT OR REPLACE 通常一个 UPSERT 应该作用于一个记录。如果您说您确定它对多条记录起作用,则相应地更改计数检查。 不好的是,如果该行存在,则无论该行是否发生变化,都必须执行更新方法。 为什么这是一件坏事?如果数据没有改变,你为什么首先打电话给UPSERT?但即便如此,更新发生是一件的事情,设置Changes=1 否则INSERT 语句会错误地触发,这是你不希望的。【参考方案4】:

所有提供的答案的问题是完全没有考虑触发器(可能还有其他副作用)。 像这样的解决方案

INSERT OR IGNORE ...
UPDATE ...

当行不存在时,导致两个触发器都执行(用于插入,然后用于更新)。

正确的解决方案是

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

在这种情况下,只执行一条语句(当行存在或不存在时)。

【讨论】:

我明白你的意思。我会更新我的问题。顺便说一句,我不知道为什么需要UPDATE OR IGNORE,因为如果没有找到行,更新不会崩溃。 可读性?我一眼就能看出安迪的代码在做什么。你的 bgusach 我不得不研究一分钟才能弄清楚。【参考方案5】:

要有一个没有漏洞的纯 UPSERT(对于程序员),不依赖唯一键和其他键:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT changes() 将返回上次查询中完成的更新次数。 然后检查changes()的返回值是否为0,如果是则执行:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 

【讨论】:

这相当于@fiznool 在他的评论中提出的建议(尽管我会寻求他的解决方案)。没关系,实际上工作正常,但是您没有唯一的 SQL 语句。不基于 PK 或其他唯一键的 UPSERT 对我来说几乎没有意义。【参考方案6】:

您也可以在您的 user_name 唯一约束中添加一个 ON CONFLICT REPLACE 子句,然后直接插入,让 SQLite 找出在发生冲突时该怎么做。见:https://sqlite.org/lang_conflict.html

还要注意关于删除触发器的句子:当 REPLACE 冲突解决策略删除行以满足约束时,删除触发器当且仅当启用递归触发器时才会触发。

【讨论】:

【参考方案7】:

选项 1:插入 -> 更新

如果您想避免changes()=0INSERT OR IGNORE,即使您无法删除该行 - 您可以使用此逻辑;

首先,插入(如果不存在),然后通过使用唯一键过滤来更新

示例

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

关于触发器

注意:我尚未对其进行测试以查看正在调用哪些触发器,但我假设如下:

如果行不存在

插入前 使用 INSTEAD OF 插入 插入后 更新前 使用 INSTEAD OF 更新 更新后

如果行确实存在

更新前 使用 INSTEAD OF 更新 更新后

选项 2:插入或替换 - 保留您自己的 ID

这样你就可以拥有一条 SQL 命令

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

编辑:添加选项 2。

【讨论】:

以上是关于SQLite UPSERT / 更新或插入的主要内容,如果未能解决你的问题,请参考以下文章

Oracle:如何 UPSERT(更新或插入表?)

SQLCE - Upsert(更新或插入) - 如何使用常用方法准备一行?

如何在SQL Server 2005中进行upsert(更新或插入)

mysql实现upsert

mongodb的更新操作符

laravel:更新或创建(更新插入)数据透视表