SQLite UPSERT /更新或插入


102

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

有INSERT或REPLACE命令,在许多情况下可能会有用。但是,如果您由于外键而希望使用自动增量功能将ID保留在原位,则该操作将无效,因为它会删除该行,创建一个新行,因此此新行具有一个新ID。

表格如下:

播放器-(ID上的主键,user_name唯一)

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

Answers:


51

这是一个较晚的答案。从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,请参考以下答案(由我发布,@ MarqueIV)。

但是,如果您确实具有升级选项,则强烈建议您这样做,因为与我的解决方案不同,此处发布的解决方案仅需一条语句即可实现所需的行为。另外,您还可以获得其他所有功能,改进和错误修复,通常是更新版本随附的。


目前,Ubuntu存储库中尚无此版本。
bl79

为什么我不能在Android上使用它?我试过了db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?")。给我“ on”一词的语法错误
Bastian Voigt

1
@BastianVoigt因为在各种版本的Android上安装的SQLite3库都早于3.24.0。请参阅:developer.android.com/reference/android/database/sqlite/…可悲的是,您需要在Android或iOS上使用SQLite3的新功能(或任何其他系统库),您需要在您的产品中捆绑特定版本的SQLite应用程序而不是依赖于系统安装之一。
prapin

因为它首先尝试插入,所以它不是UPSERT,更不是INDATE吗?;)
Mark A. Donohoe

@BastianVoigt,请参阅下面的我的答案(在上面的问题中链接),该答案适用于3.24.0之前的版本。
Mark A. Donohoe

105

问答风格

好了,在研究并解决了数小时的问题之后,我发现有两种方法可以完成此任务,具体取决于表的结构以及是否激活了外键限制以保持完整性。我想以干净的格式分享此信息,以节省一些可能遇到我的人的时间。


选项1:您可以删除该行

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

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

看下面的代码:

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(请从user_name ='steven'的玩家中选择ID )执行此更新,则不能使用此CASE(或至少我没有对其进行管理),否则请执行INSERT。不行

然后,最后我成功地使用了蛮力。逻辑是,对于您要执行的每个UPSERT,首先执行INSERT或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); 

10
同上...选项2很棒。除此之外,我反过来做了:尝试更新,检查rowsAffected> 0,如果不是,则执行插入操作。
汤姆·斯宾塞

这也是一种相当不错的方法,唯一的小缺点是“ upsert”没有一个SQL。
bgusach 2013年

2
您不需要在上一个代码示例的update语句中重新设置user_name。设定年龄就足够了。
Serg Stetsuk '18 / 10/30

72

这是一种不需要强力“忽略”的方法,只有在发生键违规时才起作用。这种方法可以根据您在更新中指定的任何条件来工作。

试试这个...

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

这个怎么运作

子句中使用Changes()的是“魔术酱” WhereChanges()表示受上一次操作影响的行数,在此情况下为更新。

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

如果Update 确实更新了现有行,则Changes()= 1(或更准确地说,如果更新了不止一行,则不为零),因此Insertnow中的“ Where”子句的计算结果为false,因此不会进行插入。

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

此外,由于它只是一个标准Where子句,因此它可以基于您定义的任何内容,而不仅仅是键冲突。同样,在允许使用Changes()表达式的任何地方,您都可以将您想要/需要的其他任何东西组合使用。


1
这对我来说很棒。除了所有INSERT或REPLACE示例外,我在其他任何地方都没有看到此解决方案,它对于我的用例而言更加灵活。
csab

@MarqueIV,如果必须更新或插入两个项目该怎么办?例如,fir已更新,而第二个不存在。在这种情况下,Changes() = 0将返回false,两行将进行插入或替换
Andriy Antonov '18

通常,一个UPSERT应该作用于一个记录。如果您说要确定它确实作用于多个记录,请相应地更改计数检查。
Mark A. Donohoe

不好的是,如果该行存在,则无论该行是否已更改,都必须执行update方法。
Jimi

1
为什么这是一件坏事?如果数据没有更改,那么为什么要首先拨打电话UPSERT?但是,即使这样,还是会发生更新,设置Changes=1否则该INSERT语句将错误地触发,这是件好事,您不希望这样做。
Mark A. Donohoe

25

所有提出的答案都存在问题,它完全没有考虑到触发器(可能还有其他副作用)。解决方案像

INSERT OR IGNORE ...
UPDATE ...

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

正确的解决方案是

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

在这种情况下,仅执行一条语句(无论是否存在行)。


1
我明白你的意思。我将更新我的问题。顺便说一句,我不知道为什么UPDATE OR IGNORE是必要的,因为如果没有行被发现更新不会崩溃。
bgusach 2015年

1
可读性?我一眼就能看到Andy的代码在做什么。你的笨拙,我不得不花一分钟时间去弄清楚。
布兰登

6

要拥有一个没有孔(对于程序员)且不依赖唯一键和其他键的纯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对我来说几乎没有意义。
bgusach

4

您还可以只在用户名唯一约束中添加ON CONFLICT REPLACE子句,然后直接插入,将其留给SQLite找出发生冲突时的处理方法。请参阅:https : //sqlite.org/lang_conflict.html

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


1

选项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。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.