如何在PostgreSQL中UPSERT(MERGE,INSERT ...在DUPLICATE UPDATE上)?

Cra*_*ger 246 postgresql upsert insert-update sql-merge

这里一个非常常见的问题是如何进行upsert,这是MySQL调用的INSERT ... ON DUPLICATE UPDATE,标准支持作为MERGE操作的一部分.

鉴于PostgreSQL不直接支持它(在第9.5页之前),你是如何做到这一点的?考虑以下:

CREATE TABLE testtable (
    id integer PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');
Run Code Online (Sandbox Code Playgroud)

现在,假设你想"UPSERT"的元组(2, 'Joe'),(3, 'Alan'),因此新表的内容是:

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing tuple
(3, 'Alan')    -- Added new tuple
Run Code Online (Sandbox Code Playgroud)

这是人们在讨论时所谈论的内容upsert.至关重要的是,任何方法在同一个表上存在多个事务时都必须是安全的 - 通过使用显式锁定,或以其他方式抵御由此产生的竞争条件.

关于PostgreSQL中的重复更新,Insert上广泛讨论了这个主题,但这是关于MySQL语法的替代品,随着时间的推移,它已经成长为一些无关的细节.我正在研究明确的答案.

这些技术对于"插入如果不存在,否则什么都不做"也很有用,即"插入...复制键忽略".

Cra*_*ger 376

9.5及更新版本:

PostgreSQL 9.5和更新的支持INSERT ... ON CONFLICT UPDATE(和ON CONFLICT DO NOTHING),即upsert.

与...比较ON DUPLICATE KEY UPDATE.

快速解释.

有关用法,请参阅手册 - 特别是语法图中的conflict_action子句和说明文本.

与下面给出的9.4及更早版本的解决方案不同,此功能适用于多个冲突的行,并且不需要独占锁定或重试循环.

添加功能的提交就在这里,围绕其开发的讨论就在这里.


如果您使用的是9.5并且不需要向后兼容,则可以立即停止阅读.


9.4岁及以上:

PostgreSQL没有任何内置UPSERT(或MERGE)功能,并且在并发使用时有效地执行它非常困难.

本文将详细讨论该问题.

通常,您必须在两个选项中进行选择:

  • 重试循环中的单独插入/更新操作; 要么
  • 锁定表并进行批量合并

单独的行重试循环

如果您希望同时尝试执行插入的许多连接,则在重试循环中使用单独的行upsert是合理的选项.

PostgreSQL文档包含一个有用的过程,它允许您在数据库内的循环中执行此操作.与大多数天真的解决方案不同,它可以防止丢失更新并插入比赛.它只能在READ COMMITTED模式下工作,但只有在交易中你做的唯一事情才是安全的.如果触发器或辅助唯一键导致唯一违规,则该功能将无法正常工作.

这种策略效率很低.在任何实际情况下,您应该排队工作并执行如下所述的批量upsert.

许多尝试解决此问题的方法都未考虑回滚,因此导致更新不完整.两笔交易相互竞争; 其中一个成功了INSERT; 另一个得到一个重复的键错误,UPDATE而是做了一个.UPDATE等待INSERT回滚或提交的块.当它回滚时,UPDATE条件重新检查匹配零行,所以即使UPDATE提交它实际上没有完成你期望的upsert.您必须检查结果行计数并在必要时重试.

一些尝试的解决方案也未考虑SELECT比赛.如果你尝试明显而简单的:

-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.

BEGIN;

UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;

-- Remember, this is WRONG. Do NOT COPY IT.

INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);

COMMIT;
Run Code Online (Sandbox Code Playgroud)

然后当两个一次运行时有几种故障模式.一个是已经讨论过的更新重新检查的问题.另一个是两者UPDATE同时匹配零行并继续.然后,他们都做EXISTS测试,这恰好之前INSERT.两者都得到零行,所以两者都做了INSERT.一个失败,出现重复键错误.

这就是你需要重试循环的原因.您可能认为可以使用聪明的SQL来防止重复键错误或丢失更新,但您不能.您需要检查行计数或处理重复键错误(取决于所选方法)并重试.

请不要为此推出自己的解决方案.与消息排队一样,它可能是错误的.

带锁的大容量upsert

有时您希望进行批量upsert,其中有一个新数据集要合并到较旧的现有数据集中.这大大超过各行upserts更高效,更应是首选,只要实用.

在这种情况下,您通常遵循以下过程:

  • CREATE一张TEMPORARY桌子

  • COPY 或将新数据批量插入临时表

  • LOCK目标表IN EXCLUSIVE MODE.这允许其他事务SELECT,但不对表进行任何更改.

  • UPDATE ... FROM使用临时表中的值执行现有记录;

  • 执行INSERT目标表中尚不存在的行;

  • COMMIT,释放锁.

例如,对于问题中给出的示例,使用多值INSERT来填充临时表:

BEGIN;

CREATE TEMPORARY TABLE newvals(id integer, somedata text);

INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');

LOCK TABLE testtable IN EXCLUSIVE MODE;

UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;

INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;

COMMIT;
Run Code Online (Sandbox Code Playgroud)

相关阅读

怎么样MERGE

SQL标准MERGE实际上具有定义不明确的并发语义,并且不适合在不先锁定表的情况下进行upsert.

这对于数据合并来说是一个非常有用的OLAP语句,但对于并发安全upsert来说它实际上并不是一个有用的解决方案.对于使用其他DBMS MERGE用于upserts的人们有很多建议,但实际上这是错误的.

其他DB:

  • Postgres 现在支持 UPSERT - http://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=168d5805e4c08bed7b95d351bf097cff7c07dd65 (2认同)
  • 例子是黄金——那为什么没有呢?实际工作示例,不是三个点的东西,是实际 UPSERT 语法正确的示例。我们可以要那个吗?编辑 - 在这里找到一些例子 https://kb.objectrocket.com/postgresql/how-to-perform-the-postgresql-upsert-in-existing-table-1260 如果在谷歌搜索时可以成为最后一站,那就太好了。编辑 #2 在下面的答案中找到了一些例子,遗憾的是它没有被接受。 (2认同)
  • @Petr 我链接到用户手册。我在你谈论的功能存在之前就写了这篇文章。想要改变吗?提出修改并添加您自己想要的示例! (2认同)

Ren*_*nzo 30

我正在努力为PostgreSQL的9.5之前版本的单插入问题提供另一种解决方案.这个想法只是尝试首先执行插入,如果记录已经存在,则更新它:

do $$
begin 
  insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
  update testtable set somedata = 'Joe' where id = 2;
end $$;
Run Code Online (Sandbox Code Playgroud)

请注意,只有在没有删除表的行时才能应用此解决方案.

我不知道这个解决方案的效率,但在我看来它足够合理.

  • 谢谢,这正是我想要的.无法理解为什么这么难找. (3认同)
  • 是的.当且仅当没有删除时,此简化才有效. (3认同)

Eri*_*ang 16

以下是insert ... on conflict ...(第9.5 +)页的一些示例:

  • 插入冲突 - 什么都不做.
    insert ... on conflict ...

  • 在冲突时插入 - 执行更新,通过指定冲突目标.
    insert ... on conflict ...

  • 在冲突时插入 - 执行更新,通过约束名称指定冲突目标.
    insert ... on conflict ...


pal*_*uke 12

PostgreSQL 中的 MERGE v.>=15

从 PostgreSQL v. 15开始,可以使用MERGE命令。它实际上是这个新版本的第一个主要改进

它使用WHEN MATCHED/WHEN NOT MATCHED条件来选择当存在具有相同条件的现有行时的行为。

它甚至比标准的 更好UPSERT,因为新功能可以完全控制 INSERT,UPDATEDELETE批量行。

MERGE INTO customer_account ca
USING recent_transactions t
ON t.customer_id = ca.customer_id
WHEN MATCHED THEN
  UPDATE SET balance = balance + transaction_value
WHEN NOT MATCHED THEN
  INSERT (customer_id, balance)
  VALUES (t.customer_id, t.transaction_value)
Run Code Online (Sandbox Code Playgroud)


P.R*_*.R. 7

Postgres> = 9.5的SQLAlchemy更新

由于上面的大篇幅介绍了Postgres版本的许多不同SQL方法(不仅是非9.5的问题),如果您使用的是Postgres 9.5,我想在SQLAlchemy中添加操作方法。除了实现自己的upsert,您还可以使用SQLAlchemy的函数(已在SQLAlchemy 1.1中添加)。就个人而言,我建议尽可能使用它们。不仅是因为方便,还因为它使PostgreSQL处理可能发生的任何竞争情况。

我昨天给出的另一个答案的交叉发布(/sf/answers/3107718841/

SQLAlchemy的支持ON CONFLICT,现在有两种方法on_conflict_do_update()on_conflict_do_nothing()

从文档中复制:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='a@b.com', data='inserted data')
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email],
    index_where=my_table.c.user_email.like('%@gmail.com'),
    set_=dict(data=stmt.excluded.data)
    )
conn.execute(stmt)
Run Code Online (Sandbox Code Playgroud)

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert

  • 问题中未提及Python和SQLAlchemy。 (3认同)

归档时间:

查看次数:

183572 次

最近记录:

6 年 前