几个月前,我从Stack Overflow上的一个回答中学到了如何在MySQL中使用以下语法一次执行多个更新:
INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);
我现在已经切换到PostgreSQL,显然这是不正确的。它引用了所有正确的表,所以我假设这是使用不同关键字的问题,但我不确定在PostgreSQL文档中这是被覆盖的。
为了澄清,我想插入一些东西,如果它们已经存在,则更新它们。
警告:如果同时从多个会话执行,这是不安全的(参见下面的警告)。
在postgresql中执行“UPSERT”的另一个聪明方法是执行两个连续的UPDATE/INSERT语句,每个语句都被设计为成功或没有效果。
UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
SELECT 3, 'C', 'Z'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
如果已经存在“id=3”的行,则UPDATE将成功,否则将不起作用。
只有当“id=3”的行不存在时,INSERT才会成功。
您可以将这两个组合到一个字符串中,并通过从应用程序执行的单个SQL语句同时运行它们。强烈建议在一个事务中同时运行它们。
This works very well when run in isolation or on a locked table, but is subject to race conditions that mean it might still fail with duplicate key error if a row is inserted concurrently, or might terminate with no row inserted when a row is deleted concurrently. A SERIALIZABLE transaction on PostgreSQL 9.1 or higher will handle it reliably at the cost of a very high serialization failure rate, meaning you'll have to retry a lot. See why is upsert so complicated, which discusses this case in more detail.
这种方法在读提交隔离中还可能导致更新丢失,除非应用程序检查受影响的行数,并验证插入或更新是否影响了行。
没有简单的命令可以做到这一点。
最正确的方法是使用函数,就像docs中的函数一样。
另一种解决方案(尽管不是那么安全)是在执行update的同时返回,检查哪些行是更新的,然后插入其余的行
大致如下:
update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;
假设返回id:2:
insert into table (id, column) values (1, 'aa'), (3, 'cc');
当然,(在并发环境中)它迟早会崩溃,因为这里有明确的竞态条件,但通常它会工作。
这里有一篇关于这个主题的更长的、更全面的文章。
在PostgreSQL 9.5及更新版本中,你可以使用INSERT…冲突更新。
请参见文档。
MySQL插入…“ON DUPLICATE KEY UPDATE”可以直接转换为“ON CONFLICT UPDATE”。它们都不是sql标准语法,它们都是特定于数据库的扩展。没有使用MERGE是有原因的,创建新语法不是为了好玩。(MySQL的语法也有问题,这意味着它没有被直接采用)。
例如,给定的设置:
CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);
MySQL查询:
INSERT INTO tablename (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
就变成:
INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;
差异:
必须指定用于惟一性检查的列名(或惟一约束名)。这是ON CONFLICT (columnname) DO
必须使用关键字SET,就好像这是一条普通的UPDATE语句一样
它也有一些不错的功能:
You can have a WHERE clause on your UPDATE (letting you effectively turn ON CONFLICT UPDATE into ON CONFLICT IGNORE for certain values)
The proposed-for-insertion values are available as the row-variable EXCLUDED, which has the same structure as the target table. You can get the original values in the table by using the table name. So in this case EXCLUDED.c will be 10 (because that's what we tried to insert) and "table".c will be 3 because that's the current value in the table. You can use either or both in the SET expressions and WHERE clause.
有关upsert的背景知识,请参阅如何upsert(合并,插入…重复更新)在PostgreSQL?
PostgreSQL >= v15
关于这个话题的大新闻是在PostgreSQL v15中,可以使用MERGE命令。事实上,这个期待已久的特性被列为v15版本的第一个改进。
这类似于INSERT…ON冲突,但更面向批处理。它具有强大的WHEN MATCHED vs WHEN NOT MATCHED结构,可以在这种情况下执行INSERT、UPDATE或DELETE操作。
它不仅简化了批量更改,而且甚至比传统的UPSERT和INSERT增加了更多的控制…在冲突
看看这个非常完整的样本从官方页面:
MERGE INTO wines w
USING wine_stock_changes s
ON s.winename = w.winename
WHEN NOT MATCHED AND s.stock_delta > 0 THEN
INSERT VALUES(s.winename, s.stock_delta)
WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
UPDATE SET stock = w.stock + s.stock_delta
WHEN MATCHED THEN
DELETE;
PostgreSQL v9, v10, v11, v12, v13, v14
如果版本低于v15而高于v9.5,可能最好的选择是使用UPSERT语法,带有ON CONFLICT子句