如何在PostgreSQL中进行UPSERT(MERGE,INSERT…ON DUPLICATE UPDATE)?


267

这里一个非常常见的问题是如何进行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');

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

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing tuple
(3, 'Alan')    -- Added new tuple

那就是人们在讨论时谈论的话题upsert。至关重要的是,在存在多个事务处理同一个表的情况下,任何方法都必须安全 -通过使用显式锁定或以其他方式抵御由此产生的竞争条件。

插入中,关于PostgreSQL中的重复更新,对该主题进行了广泛讨论,但这是关于MySQL语法的替代方法,并且随着时间的推移,它已经发展出了很多无关的细节。我正在努力确定答案。

这些技术还可用于“如果不存在则插入,否则不执行任何操作”,即“在重复键忽略时插入...”。



8
@MichaelHampton的目标是创建一个明确的版本,该版本不会被多个过时的答案所迷惑-并且已锁定,因此没有人可以对此做任何事情。我不同意这种观点。
克雷格·林格

为什么,然后这很快就会过时-并被锁定,因此没人能做任何事情。
迈克尔·汉普顿

2
@MichaelHampton如果您担心的话,也许您可​​以标记链接到的那个并要求将其解锁以便可以对其进行清理,然后我们可以将其合并。 up-up的as-dup是如此令人困惑和错误的混乱。
Craig Ringer 2014年

1
问答未锁定!
迈克尔·汉普顿

Answers:


396

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)功能,面对并发使用要高效地执行它非常困难。

本文详细讨论了该问题

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

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

个别行重试循环

如果您希望多个连接同时尝试执行插入操作,则在重试循环中使用单个行向上插入是合理的选择。

PostgreSQL文档包含一个有用的过程,可让您在数据库内部循环执行此操作。与大多数幼稚的解决方案不同,它可以防止丢失更新和插入竞争。但是,它将仅在READ COMMITTED模式下工作,并且只有在事务中唯一执行时,它才是安全的。如果触发器或辅助唯一键导致唯一违规,则该功能将无法正常工作。

此策略效率很低。只要可行,您都应该将工作排入队列,并按如下所述进行批量追加。

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

一些尝试的解决方案也没有考虑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;

那么当两个同时运行时,会出现几种故障模式。一个问题是已经讨论过的更新重新检查问题。另一个是两个都UPDATE同时匹配零行并继续的地方。然后,他们都做EXISTS测试,这恰好之前INSERT。两者都获得零行,因此都获得INSERT。一个失败,重复密钥错误。

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

请不要为此提出自己的解决方案。像消息队列一样,这可能是错误的。

带锁的批量更新

有时您想做一个批量上载,在这里您有一个新数据集要合并到一个旧的现有数据集中。这大大超过各行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;

相关阅读

MERGE

SQL标准MERGE实际上具有定义不明确的并发语义,因此不先锁定表就不适合进行上载。

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

其他数据库:


在批量更新中,从newval中删除而不是过滤INSERT是否有可能具有价值?例如,使用upd AS(更新...返回newvals.id),使用upd WHERE newvals.id = upd.id从newvals中删除,然后在裸露的INSERT INTO testtable SELECT * FROM newvals中插入?我的想法是:不要在INSERT中过滤两次(对于JOIN / WHERE和唯一约束),而是重用UPDATE中的存在检查结果,该结果已经在RAM中,并且可能会小得多。如果匹配的行很少和/或newval比testtable小得多,这可能是一个胜利。
Gunnlaugur Briem 2014年

1
仍然存在未解决的问题,对于其他供应商,尚不清楚什么有效,什么无效。1.提到的Postgres循环解决方案在多个唯一键的情况下不起作用。2. mysql的onplicate键也不适用于多个唯一键。3.以上发布的针对MySQL,SQL Server和Oracle的其他解决方案是否有效?在这种情况下是否可能出现异常,我们是否需要循环?
丹b

@danb这仅与PostgreSQL有关。没有跨供应商的解决方案。PostgreSQL的解决方案不适用于多行,不幸的是,您必须每行执行一次事务。如上所述,MERGE用于SQL Server和Oracle 的“解决方案” 是不正确的,并且容易出现争用情况。您需要专门研究每个DBMS,以了解如何处理它们,我实际上只能在PostgreSQL上提供建议。在PostgreSQL上进行安全多行upsert的唯一方法是将对本机upsert的支持添加到核心服务器。
克雷格·林格

即使对于PostGresQL,该解决方案在表具有多个唯一键(仅更新一行)的情况下也不起作用。在这种情况下,您需要指定要更新的密钥。例如,可能存在使用jdbc的跨供应商解决方案。
dan


32

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

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

请注意,只有在不删除表的行的情况下,才可以应用此解决方案。

我不知道这种解决方案的效率,但是在我看来这足够合理。


3
谢谢,这正是我想要的。无法理解为什么很难找到它。
isapir

3
是的 当且仅当没有删除时,这种简化才有效。
Craig Ringer

@CraigRinger您能解释一下如果删除了该怎么办?
turbanoff

@turbanoff插入操作可能会失败,因为该记录已经存在,然后会同时删除它,然后更新会影响零行,因为该行已删除。
克雷格·林格

@CraigRinger所以 删除是同时发生的。什么是可能的outways如果这工作正常?如果删除是同时进行的,则可以在我们的代码块之后立即执行删除。我要说的是-如果我们有并发删除功能-那么此代码会以与正常情况相同的方式启动insert on update
turbanoff

28

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

  • 发生冲突时插入- 不执行任何操作
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict do nothing;`  
  • 插入后,在进行冲突更新时,通过column指定冲突目标。
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict(id)
    do update set name = 'new_name', size = 3;  
  • 在执行冲突更新时插入,通过约束名称指定冲突目标。
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict on constraint dummy_pkey
    do update set name = 'new_name', size = 4;

很好的答案-问题:为什么要在哪种情况下通过列或约束名称使用目标规范?各种用例是否有优点/缺点?
内森·本顿

1
@NathanBenton我认为至少有两个区别:(1)列名由程序员指定,而约束名称可以由程序员指定,或者由数据库根据表/列名生成。(2)每列可能有多个约束。就是说,取决于您的情况来选择使用哪个。
Eric Wang

8

Postgres> = 9.5的SQLAlchemy更新

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

我昨天给出的另一个答案的交叉发布(https://stackoverflow.com/a/44395983/2156909

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)

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


4
问题中未提及Python和SQLAlchemy。
亚历山大·埃梅利安诺夫

我经常在编写的解决方案中使用Python。但是我还没有研究过SQLAlchemy(或者已经意识到了)。这似乎是一个不错的选择。谢谢。如果签出,我将把它呈现给我的组织。
罗伯特

3
WITH UPD AS (UPDATE TEST_TABLE SET SOME_DATA = 'Joe' WHERE ID = 2 
RETURNING ID),
INS AS (SELECT '2', 'Joe' WHERE NOT EXISTS (SELECT * FROM UPD))
INSERT INTO TEST_TABLE(ID, SOME_DATA) SELECT * FROM INS

在Postgresql 9.3上测试


@CraigRinger:您能详细说明一下吗?CTE不是原子的吗?
parisni

2
@parisni否。每个CTE术语如果执行写操作,都会获得自己的快照。而且,没有对找到的行执行谓词锁定,因此它们仍可以由另一个会话同时创建。如果使用SERIALIZABLE隔离,则会因序列化失败而中止,否则您可能会遇到独特的违规情况。不要重新发明高手,重新发明将是错误的。使用INSERT ... ON CONFLICT ...。如果您的PostgreSQL过旧,请对其进行更新。
克雷格·林格

@CraigRinger INSERT ... ON CLONFLICT ...不适用于批量加载。根据您的帖子,LOCK TABLE testtable IN EXCLUSIVE MODE;CTE中的可以解决原子问题。不是吗
parisni '19

@parisni不是用于批量加载吗?谁说的?postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT。当然,它比没有类似upsert的行为的批量加载要慢得多,但这是显而易见的,无论您做什么,情况都是如此。可以肯定,它比使用子事务更快。当然,最快的方法是锁定目标表然后执行一个insert ... where not exists ...或类似操作。
Craig Ringer

1

由于此问题已经关闭,因此我将在此处发布有关如何使用SQLAlchemy进行处理的信息。通过递归,它重试批量插入或更新以适应比赛条件和验证错误。

首先进口

import itertools as it

from functools import partial
from operator import itemgetter

from sqlalchemy.exc import IntegrityError
from app import session
from models import Posts

现在有几个助手功能

def chunk(content, chunksize=None):
    """Groups data into chunks each with (at most) `chunksize` items.
    https://stackoverflow.com/a/22919323/408556
    """
    if chunksize:
        i = iter(content)
        generator = (list(it.islice(i, chunksize)) for _ in it.count())
    else:
        generator = iter([content])

    return it.takewhile(bool, generator)


def gen_resources(records):
    """Yields a dictionary if the record's id already exists, a row object 
    otherwise.
    """
    ids = {item[0] for item in session.query(Posts.id)}

    for record in records:
        is_row = hasattr(record, 'to_dict')

        if is_row and record.id in ids:
            # It's a row but the id already exists, so we need to convert it 
            # to a dict that updates the existing record. Since it is duplicate,
            # also yield True
            yield record.to_dict(), True
        elif is_row:
            # It's a row and the id doesn't exist, so no conversion needed. 
            # Since it's not a duplicate, also yield False
            yield record, False
        elif record['id'] in ids:
            # It's a dict and the id already exists, so no conversion needed. 
            # Since it is duplicate, also yield True
            yield record, True
        else:
            # It's a dict and the id doesn't exist, so we need to convert it. 
            # Since it's not a duplicate, also yield False
            yield Posts(**record), False

最后是upsert功能

def upsert(data, chunksize=None):
    for records in chunk(data, chunksize):
        resources = gen_resources(records)
        sorted_resources = sorted(resources, key=itemgetter(1))

        for dupe, group in it.groupby(sorted_resources, itemgetter(1)):
            items = [g[0] for g in group]

            if dupe:
                _upsert = partial(session.bulk_update_mappings, Posts)
            else:
                _upsert = session.add_all

            try:
                _upsert(items)
                session.commit()
            except IntegrityError:
                # A record was added or deleted after we checked, so retry
                # 
                # modify accordingly by adding additional exceptions, e.g.,
                # except (IntegrityError, ValidationError, ValueError)
                db.session.rollback()
                upsert(items)
            except Exception as e:
                # Some other error occurred so reduce chunksize to isolate the 
                # offending row(s)
                db.session.rollback()
                num_items = len(items)

                if num_items > 1:
                    upsert(items, num_items // 2)
                else:
                    print('Error adding record {}'.format(items[0]))

使用方法如下

>>> data = [
...     {'id': 1, 'text': 'updated post1'}, 
...     {'id': 5, 'text': 'updated post5'}, 
...     {'id': 1000, 'text': 'new post1000'}]
... 
>>> upsert(data)

它具有的优势bulk_save_objects在于,它可以处理插入时的关系,错误检查等(与批量操作不同)。


在我看来,这也是错误的。如果在您收集ID列表后并发会话插入一行怎么办?还是删除一个?
Craig Ringer

好点@CraigRinger我做类似的事情,但是只有1个会话执行这项工作。那么,处理多个会话的最佳方法是什么?可能是交易吗?
reubano

事务并不是解决所有并发问题的灵丹妙药。您可以使用SERIALIZABLE 事务并处理序列化失败,但这很慢。您需要错误处理和重试循环。请参阅我的答案和其中的“相关阅读”部分。
克雷格·林格

@CraigRinger陷阱。由于其他验证失败,我实际上在自己的情况下实现了重试循环。我将相应地更新此答案。
reubano
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.