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

时间:2013-06-24 02:56:05

标签: postgresql insert-update upsert 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');

现在假设你要“upsert”元组(2, 'Joe')(3, 'Alan'),所以新的表格内容将是:

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

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

这个主题在Insert, on duplicate update in PostgreSQL?进行了广泛讨论,但这是关于MySQL语法的替代方案,随着时间的推移,它已经成长为一些不相关的细节。我正在研究明确的答案。

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

6 个答案:

答案 0 :(得分:358)

9.5及更新版本:

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

Comparison with ON DUPLICATE KEY UPDATE

Quick explanation

有关用法,请参阅the manual - 特别是语法图中的 conflict_action 子句和the explanatory text

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

The commit adding the feature is herethe discussion around its development is here


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


9.4及更早版本:

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

This article discusses the problem in useful detail

一般来说,您必须在两个选项中进行选择:

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

单个行重试循环

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

The PostgreSQL documentation contains a useful procedure that'll let you do this in a loop inside the database。与大多数天真的解决方案不同,它可以防止丢失更新并插入比赛。它只能在READ COMMITTED模式下工作,但只有在交易中你做的唯一事情才是安全的。如果触发器或辅助唯一键导致唯一的违规,该功能将无法正常工作。

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

此问题的许多尝试解决方案都未考虑回滚,因此会导致更新不完整。两笔交易相互竞争;其中一个成功INSERT s;另一个得到重复的密钥错误并改为UPDATEUPDATE块等待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;

然后当两次运行时有几种故障模式。一个是已经讨论过的更新重新检查的问题。另一个是UPDATE同时在哪里,匹配零行并继续。然后他们都进行EXISTS测试,在<{em> 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;

相关阅读

MERGE怎么样?

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

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

其他数据库:

答案 1 :(得分:28)

我正在努力为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 $$;

请注意,只有在没有删除表的行时,才能应用

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

答案 2 :(得分:11)

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

  • 插入,发生冲突 - 不执行任何操作 insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict do nothing;

  • 插入,发生冲突 - 执行更新,通过指定冲突目标。
    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;

答案 3 :(得分: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上测试

答案 4 :(得分:2)

Postgres的SQLAlchemy upsert&gt; = 9.5

由于上面的大帖子涵盖了Postgres版本的许多不同的SQL方法(不仅仅是问题中的非9.5),如果你使用Postgres 9.5,我想在SQLAlchemy中添加如何做到这一点。您也可以使用SQLAlchemy的函数(在SQLAlchemy 1.1中添加),而不是实现自己的upsert。就个人而言,如果可能的话,我建议使用这些。不仅因为方便,还因为它让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

答案 5 :(得分:0)

this question关闭以来,我在这里发布了如何使用SQLAlchemy进行此操作。通过递归,它会重试批量插入或更新以对抗race conditions和验证错误。

首先是进口

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的优势在于它可以处理插入时的关系,错误检查等(与bulk operations不同)。