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

How to UPSERT (MERGE, INSERT … ON DUPLICATE UPDATE) in PostgreSQL?

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

考虑到PostgreSQL不直接支持它(在第9.5页之前),您如何做到这一点?考虑以下事项:

1
2
3
4
5
6
7
8
CREATE TABLE testtable (
    id INTEGER PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');

现在假设您想要"推翻"tuples (2, 'Joe')(3, 'Alan'),那么新的表内容将是:

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

这就是人们在讨论upsert时所说的。至关重要的是,在同一个表上存在多个事务的情况下,任何方法都必须是安全的——要么使用显式锁定,要么用其他方式防御由此产生的争用条件。

这个主题在insert,on duplicate update in postgresql中被广泛讨论?但这是关于MySQL语法的替代方案,随着时间的推移,它变得相当不相关。我正在研究确定的答案。

这些技术对于"如果不存在则插入,否则不做任何事情"也很有用,即"插入…在重复键上忽略"。


9.5和更新:

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

ON DUPLICATE KEY UPDATE比较。好的。

快速解释。好的。

有关用法,请参阅手册-特别是语法图中的冲突操作子句和解释性文本。好的。

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

这里是提交添加特性,这里是关于其开发的讨论。好的。

如果你在9.5上,不需要向后兼容,你现在可以停止阅读了。好的。9.4岁及以上:

PostgreSQL没有任何内置的UPSERTMERGE功能,在面临并发使用的情况下很难有效地进行。好的。

本文详细讨论了这个问题。好的。

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

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

单行重试循环

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

PostgreSQL文档包含一个有用的过程,可以让您在数据库内部的循环中完成这项工作。与大多数天真的解决方案不同,它可以防止丢失的更新和插入竞争。它只在READ COMMITTED模式下工作,并且只有在您在事务中所做的唯一事情时才是安全的。如果触发器或辅助唯一键导致唯一冲突,该函数将无法正常工作。好的。

这种策略效率很低。只要可行,你就应该把工作排成队列,然后按照下面的描述进行批量更新。好的。

许多试图解决此问题的解决方案没有考虑回滚,因此会导致不完整的更新。两个事务相互竞争;一个事务成功地执行了INSERT;另一个事务得到了重复的键错误,并执行了UPDATEUPDATE块等待INSERT回滚或提交。当它回滚时,UPDATE条件重新检查与零行匹配,因此即使UPDATE承诺它实际上没有完成您预期的更新。您必须检查结果行计数,并在必要时重试。好的。

一些尝试的解决方案也未能考虑选择种族。如果你尝试明显和简单的:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 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防止重复的键错误或丢失更新,但不能。您需要检查行计数或处理重复的键错误(取决于所选方法),然后重试。好的。

请不要用自己的方法来解决这个问题。就像消息队列一样,这可能是错误的。好的。带锁的大容量提升

有时,您希望执行批量更新,其中有一个新的数据集要合并到旧的现有数据集中。这比单独的行升迁效率要高得多,并且在任何可行的情况下都应该优先考虑。好的。

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

  • CREATEa TEMPORARY表好的。

  • COPY或大容量插入新数据到临时表中好的。

  • LOCK目标表IN EXCLUSIVE MODE。这允许其他事务处理到SELECT,但不更改表。好的。

  • 使用临时表中的值对现有记录执行UPDATE ... FROM操作;好的。

  • 对目标表中不存在的行执行INSERT;好的。

  • COMMIT,释放锁。好的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

相关阅读

  • 更新Wiki页面
  • Postgres中的上升
  • 在PostgreSQL中重复更新时插入?
  • http://petereisentraut.blogspot.com/2010/05/merge-syntax.html
  • 用事务升迁
  • 在函数中选择或插入是否容易出现竞争条件?
  • PostgreSQL wiki上的SQL MERGE
  • 在PostgreSQL中实现upsert的最常用方法

那么MERGE呢?

SQL标准MERGE实际上具有定义不好的并发语义,不适合在不首先锁定表的情况下进行升迁。好的。

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

  • MySQL中的INSERT ... ON DUPLICATE KEY UPDATE
  • 来自MS SQL Server的MERGE(但请参阅上面关于MERGE问题的内容)
  • 来自Oracle的MERGE(但见上文关于MERGE问题)

好啊。


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

1
2
3
4
5
6
do $$
BEGIN
  INSERT INTO testtable(id, somedata) VALUES(2,'Joe');
exception WHEN unique_violation THEN
  UPDATE testtable SET somedata = 'Joe' WHERE id = 2;
END $$;

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

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


以下是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;


1
2
3
4
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上测试


Postgres的SQLAlchemy Upsert>=9.5

由于上面的大型文章涵盖了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()两种:

从文档中复制:

1
2
3
4
5
6
7
8
9
FROM sqlalchemy.dialects.postgresql import INSERT

stmt = INSERT(my_table).values(user_email='[email protected]', 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.sqlacchemy.org/en/latest/dialogens/postgresql.html?突出显示=冲突冲突时插入upsert


既然这个问题已经解决了,我将在这里介绍如何使用sqlacalchemy。通过递归,它重试大容量插入或更新,以对抗竞争条件和验证错误。

首先是进口

1
2
3
4
5
6
7
8
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

现在有几个助手函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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]))

这是你使用它的方法

1
2
3
4
5
6
>>> DATA = [
...     {'id': 1, 'text': 'updated post1'},
...     {'id': 5, 'text': 'updated post5'},
...     {'id': 1000, 'text': 'new post1000'}]
...
>>> upsert(DATA)

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