如果不存在则插入,否则更新?

356

我发现一些针对典型问题“如何在记录不存在时插入新记录,否则更新已有记录”的解决方案,但是我不能让任何一个在SQLite中起作用。

我定义了一个表,如下所示:

CREATE TABLE Book 
ID     INTEGER PRIMARY KEY AUTOINCREMENT,
Name   VARCHAR(60) UNIQUE,
TypeID INTEGER,
Level  INTEGER,
Seen   INTEGER

我想做的是添加一个带有唯一名称的记录。如果该名称已经存在,我想修改字段。

请问有人可以告诉我如何实现吗?


7
"插入或替换"与"插入或更新"是完全不同的。 - Fattie
3
UPSERT是什么意思? - ashleedawg
这个回答解决了你的问题吗?SQLite - UPSERT *not* INSERT or REPLACE - user4157124
9个回答

380

请查看http://sqlite.org/lang_conflict.html

您需要类似以下内容:

insert or replace into Book (ID, Name, TypeID, Level, Seen) values
((select ID from Book where Name = "SearchName"), "SearchName", ...);
请注意,如果行已经存在于表中,则插入列表中不存在的任何字段都将设置为 NULL。这就是为什么在替换的情况下,该语句会将其设置为 NULL,然后分配一个新的 ID 的子选择器。
如果您要在替换的情况下保留特定字段值,但在插入的情况下将字段设置为 NULL,则也可以使用此方法。
例如,假设您想保留“Seen”字段:
insert or replace into Book (ID, Name, TypeID, Level, Seen) values (
   (select ID from Book where Name = "SearchName"),
   "SearchName",
    5,
    6,
    (select Seen from Book where Name = "SearchName"));

135
“插入或替换”与“插入或更新”是不同的。要得到正确的答案,请参考https://dev59.com/k3RC5IYBdhLWcg3wD87r。 - rds
17
不,这不是错误的,因为这个问题说的是“修改字段”,而主键不是列清单的一部分,但所有其他字段都是。如果你要处理一些边缘情况,比如没有替换所有字段值,或者如果你在操作主键,那么你应该采取不同的方法。如果你有一组完整的新字段,那么这种方法就很好。你有我看不到的特定问题吗? - janm
13
如果您知道所有字段的新值,则此方法是有效的。如果用户只更新了Level这个字段,那么这种方法就行不通了。 - rds
4
“正确”。另一个答案也相关,但这种方法适用于更新所有字段(除了键)的内容。 - Ярослав Рахматуллин
3
作为一个陷阱,您可能希望添加的是,使用INSERT OR REPLACE将触发使用被替换记录作为外键的其他表上的ON DELETE CASCADE子句。结果有些令人困惑,直到你意识到"INSERT OR REPLACE"的行为方式(按照逻辑)是"如果存在则删除,然后插入",这会触发一些在记录删除时起作用的东西。 - Mike
显示剩余6条评论

126
你应该使用 INSERT OR IGNORE 命令,紧接着使用一个 UPDATE 命令: 在下面的例子中,name 是主键:
INSERT OR IGNORE INTO my_table (name, age) VALUES ('Karen', 34)
UPDATE my_table SET age = 34 WHERE name='Karen'

第一条命令将插入记录。如果记录已经存在,它将忽略与现有主键冲突所造成的错误。

第二条命令将更新记录(现在它肯定存在)



9
在什么时候它会被忽略?当姓名和年龄都相同时吗? - Arnaud Aliès
这应该是解决方案......如果您在插入上使用任何触发器,接受的答案会每次都触发。这个不会,只执行更新操作。 - PodTech.io
1
它仅基于名称进行忽略。请记住,只有“名称”列是主键。 - Gabriel Ferrer
4
当记录是新的时候,更新并不是必需的,但仍然会执行更新操作,这会导致性能下降。 - MarcG
@GabrielFerrer 但是“name”列不是主键。 - EmreAkkoc
显示剩余3条评论

79

你需要在表上设置一个约束条件来触发"冲突",然后通过替换来解决它:

CREATE TABLE data   (id INTEGER PRIMARY KEY, event_id INTEGER, track_id INTEGER, value REAL);
CREATE UNIQUE INDEX data_idx ON data(event_id, track_id);

那么您可以执行以下操作:

INSERT OR REPLACE INTO data VALUES (NULL, 1, 2, 3);
INSERT OR REPLACE INTO data VALUES (NULL, 2, 2, 3);
INSERT OR REPLACE INTO data VALUES (NULL, 1, 2, 5);

"SELECT * FROM data"将会给你:

2|2|2|3.0
3|1|2|5.0

请注意,数据的id是“3”,而不是“1”,因为REPLACE执行的是DELETE和INSERT操作,而不是UPDATE。这也意味着您必须确保定义所有必要的列,否则将获得意外的NULL值。


51

INSERT OR REPLACE会将其他字段替换为默认值。

--- sample table ---

CREATE TABLE Book (
  ID     INTEGER PRIMARY KEY AUTOINCREMENT,
  Name   TEXT,
  TypeID INTEGER,
  Level  INTEGER,
  Seen   INTEGER
);

INSERT INTO Book VALUES (1001, 'C++', 10, 10, 0);

sqlite> INSERT OR REPLACE INTO Book(ID, Name) VALUES(1001, 'SQLite');

--- after INSERT OR REPLACE, other fields are changed to default (null)
sqlite> SELECT * FROM Book;
1001|SQLite|||

如果您想保留其他字段

  • 方法1
sqlite> SELECT * FROM Book;
1001|C++|10|10|0

--- Create record for ID = 1001. If insert failed, just ignore
sqlite> INSERT OR IGNORE INTO Book(ID) VALUES(1001);

--- Set other field value
sqlite> UPDATE Book SET Name='SQLite' WHERE ID=1001;

sqlite> SELECT * FROM Book;
1001|SQLite|10|10|0
  • 方法2

使用UPSERTUPSERT在SQLite 3.24.0版本(2018-06-04)中添加)

INSERT INTO Book (ID, Name)
  VALUES (1001, 'SQLite')
  ON CONFLICT (ID) DO
  UPDATE SET Name=excluded.Name;

前缀excluded.等于VALUES中的值('SQLite')。


44

首先更新它。如果受影响行数=0,则插入它。这是最简单和适用于所有RDBMS的方法。


12
在正确的隔离级别下执行两个操作不应该成为交易的问题,无论使用哪种数据库。 - janm
7
"插入或替换" 更可取。 - MPelletier
2
我真的希望IT文件能包含更多示例。我已经尝试了以下内容,但它没有起作用(我的语法显然是错的)。有任何想法应该是什么吗?INSERT INTO Book (Name,TypeID,Level,Seen) VALUES('Superman','2','14','0') ON CONFLICT(Name) DO UPDATE SET TypeID='2', Level='14', Seen='0' - SparkyNZ
7
如果行值完全相同,则受影响的行数将为零,并且将创建一个新的重复行。 - barkside
3
+1,因为INSERT OR REPLACE会在冲突时删除原始行,如果您没有设置所有列,则会丢失原始值。 - Pavel Machyniak
显示剩余3条评论

38

Upsert是你想要的。SQLite在3.24.0版本中(2018-06-04)添加了UPSERT语法。

CREATE TABLE phonebook2(
  name TEXT PRIMARY KEY,
  phonenumber TEXT,
  validDate DATE
);

INSERT INTO phonebook2(name,phonenumber,validDate)
  VALUES('Alice','704-555-1212','2018-05-08')
  ON CONFLICT(name) DO UPDATE SET
    phonenumber=excluded.phonenumber,
    validDate=excluded.validDate
  WHERE excluded.validDate>phonebook2.validDate;

请注意,此时实际单词"UPSERT"不是upsert语法的一部分。

正确的语法是

INSERT INTO ... ON CONFLICT(...) DO UPDATE SET...

如果你正在做INSERT INTO SELECT ...,你的select需要至少 WHERE true 才能解决关于带有联接语法的标记ON的解析器歧义。

请注意,INSERT OR REPLACE...将在插入新记录之前删除记录以进行替换,如果您有外键级联或其他删除触发器,则可能会出现问题。


它存在于文档中,我有最新版本的SQLite,即3.25.1,但对我来说不起作用。 - Bentaiba Miled Basma
你是否按原样尝试了上面那两个查询? - ComradeJoecool
1
请注意,Ubuntu 18.04自带的SQLite3版本为v3.22,而不是3.25,因此不支持UPSERT语法。 - starbeamrainbowlabs
1
这个方法运作良好,是文档中推荐的支持方法。 - AutoBaker

8
如果没有主键,您可以插入不存在的记录,然后进行更新。在使用此方法之前,表必须至少包含一条记录。
INSERT INTO Test 
   (id, name)
   SELECT 
      101 as id, 
      'Bob' as name
   FROM Test
       WHERE NOT EXISTS(SELECT * FROM Test WHERE id = 101 and name = 'Bob') LIMIT 1;

Update Test SET id='101' WHERE name='Bob';

这是唯一一个对我有效且没有创建重复条目的解决方案。可能是因为我使用的表没有主键,并且有两列没有默认值。尽管这个解决方案有点冗长,但它可以很好地完成工作并按预期工作。 - Inbar Rose
1
将“更新”语句移到“插入”语句之前,以避免插入后更新的问题。 - Carlos Liu

5
我相信你想要UPSERT
“INSERT OR REPLACE”没有其他诡计的话会重置你没有指定为NULL或其他默认值的任何字段。(这个INSERT OR REPLACE的行为不像UPDATE;它就是INSERT,因为它实际上就是INSERT;但是如果你想要的是UPDATE-if-exists,你可能想要UPDATE语义,并且会对实际结果感到不愉快。)
建议的UPSERT实现中的诡计基本上是使用INSERT OR REPLACE,但指定所有字段,使用嵌入式SELECT子句检索您不想更改的字段的当前值。

1
我认为值得指出的是,如果您不彻底了解 PRIMARY KEYUNIQUE 之间的交互作用,可能会出现一些意外的行为。
例如,如果您想要插入一条记录,仅当 NAME 字段当前未被占用时,如果被占用,则希望引发约束异常来告诉您,那么 INSERT OR REPLACE 将不会抛出异常,而是通过替换冲突记录(具有相同 NAME 的现有记录)来解决 UNIQUE 约束。Gaspard's他在上面的回答中 很好地演示了这一点。
如果要引发约束异常,必须使用 INSERT 语句,并依靠单独的 UPDATE 命令在知道名称未被占用后更新记录。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接