如何使用返回与ON冲突在PostgreSQL?

我有以下UPSERT在PostgreSQL 9.5:

INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3),
($2, $1, NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

如果没有冲突,则返回如下内容:

----------
| id |
----------
1 | 50 |
----------
2 | 51 |
----------

但是如果有冲突,它不会返回任何行:

----------
| id |
----------

如果没有冲突,我想返回新的id列,或者返回冲突列的现有id这能做到吗?如果是,如何?

143582 次浏览

Upsert是INSERT查询的扩展,在约束冲突的情况下可以用两种不同的行为定义:DO NOTHINGDO UPDATE

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
ON CONFLICT DO NOTHING RETURNING *;


id | sub_id | status
----+--------+--------
(0 rows)

还要注意RETURNING没有返回任何东西,因为没有插入元组.;现在使用DO UPDATE,可以在与之冲突的元组上执行操作。首先请注意,定义一个约束是很重要的,它将用于定义存在冲突。

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
DO UPDATE SET status = 'upserted' RETURNING *;


id | sub_id |  status
----+--------+----------
2 |      2 | upserted
(1 row)

我也遇到了同样的问题,我使用“做更新”而不是“什么都不做”来解决它,尽管我没有什么可更新的。在你的情况下,它会是这样的:

INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3),
($2, $1, NULL)
ON CONFLICT("user", "contact")
DO UPDATE SET
name=EXCLUDED.name
RETURNING id;

这个查询将返回所有的行,无论它们是刚刚插入的还是之前已经存在的。

当前接受的答案似乎可以用于单个冲突目标,很少冲突,小元组和没有触发器。它使用蛮力避免并发问题1(见下文)。简单的解决方案有它的吸引力,副作用可能不那么重要。

不过,对于所有其他情况,<强> < / >强不不需要更新相同的行。即使你在表面上看不到任何区别,也有各种副作用:

  • 它可能触发不应该触发的触发器。

  • 它写锁"无辜"行,可能会导致并发事务的成本。

  • 它可能使行看起来是新的,尽管它是旧的(事务时间戳)。

  • 最重要的是,使用PostgreSQL的MVCC模型 UPDATE为每个目标行写入一个新的行版本,无论行数据是否发生变化。这会导致UPSERT本身的性能损失,表膨胀,索引膨胀,对表的后续操作的性能损失,VACUUM成本。一个轻微的影响,少数重复,但巨大的大部分dupes。

+,有时使用ON CONFLICT DO UPDATE是不实际的,甚至是不可能的。手册:

对于ON CONFLICT DO UPDATE,必须提供conflict_target

一个 &;冲突目标;如果涉及多个索引/约束,则不可能。但这里有一个多部分索引的相关解决方案:

回到主题,你可以(几乎)达到相同的效果,而不会出现空洞的更新和副作用。下面的一些解决方案也适用于ON CONFLICT DO NOTHING (no "冲突目标"),以捕捉可能出现的所有可能的冲突——这可能是可取的,也可能不是可取的。

没有并发写负载

WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id  --, usr, contact              -- return more columns?
)
SELECT 'i' AS source                           -- 'i' for 'inserted'
, id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
, c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

source列是可选添加的,用于演示如何工作。实际上,您可能需要它来区分这两种情况(相对于空写入的另一个优点)。

最后的JOIN chats可以工作,因为从附加的data-modifying CTE中新插入的行在底层表中还不可见。(同一个SQL语句的所有部分都看到底层表的相同快照。)

由于VALUES表达式是独立的(不直接附加到INSERT), Postgres不能从目标列派生数据类型,您可能必须添加显式的类型强制转换。手册:

VALUESINSERT中使用时,所有值都是自动的 强制转换为相应目标列的数据类型。当 在其他上下文中使用时,可能有必要指定 正确的数据类型。如果条目都是带引号的文字常量, 强制使用第一个类型足以确定所有类型的假定类型

由于CTE的开销和额外的SELECT(这应该很便宜,因为根据定义,完美的索引就在那里——索引实现了唯一的约束),对于 dupes来说,查询本身(不计算副作用)可能会更昂贵一些。

对于许多副本可能(快得多)。额外写操作的有效成本取决于许多因素。

但是无论如何都有更少的副作用和隐性成本。总体来说可能更便宜。

附加序列仍然是高级序列,因为默认值在之前测试中进行冲突填充。

ct:

具有并发写负载

假设默认READ COMMITTED事务隔离。相关:

抵御竞争条件的最佳策略取决于确切的需求、表和UPSERTs中的行数和大小、并发事务的数量、冲突的可能性、可用资源和其他因素……

并发问题1

如果一个并发事务已经写入了一行,而您的事务现在试图UPSERT,那么您的事务必须等待另一个事务完成。

如果另一个事务以ROLLBACK结束(或任何错误,即自动ROLLBACK),您的事务可以正常进行。次要可能的副作用:连续数字的差距。但是没有漏行。

如果其他事务正常结束(隐式或显式COMMIT),你的INSERT将检测到冲突(UNIQUE索引/约束是绝对的)和DO NOTHING,因此也不返回行。(也不能像下面并发问题2中演示的那样锁定行,因为它是不可见的。)SELECT从查询开始时看到相同的快照,也不能返回尚不可见的行。

结果集中没有这样的行(即使它们存在于底层表中)!< / >强

可能还可以吗。特别是如果您不像示例中那样返回行,并且满足于知道行在那里。如果这还不够好,还有很多方法可以解决这个问题。

您可以检查输出的行数,如果它与输入的行数不匹配,则重复该语句。可能对罕见的情况来说足够了。重点是启动一个新的查询(可以在同一个事务中),然后该查询将看到新提交的行。

检查缺少的结果行相同的查询和覆盖那些在Alextoni的回答中演示的暴力技巧。

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON     CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact                   -- we need unique columns for later join
)
, sel AS (
SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
, id, usr, contact
FROM   ins
UNION  ALL
SELECT 's'::"char" AS source                 -- 's' for 'selected'
, c.id, usr, contact
FROM   input_rows
JOIN   chats c USING (usr, contact)
)
, ups AS (                                      -- RARE corner case
INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
SELECT i.*
FROM   input_rows i
LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
WHERE  s.usr IS NULL                         -- missing!
ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
SET    name = c.name                         -- ... this time we overwrite with old value
-- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
RETURNING 'u'::"char" AS source              -- 'u' for updated
, id  --, usr, contact               -- return more columns?
)
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

它类似于上面的查询,但在返回< >强完成< / >强结果集之前,我们使用CTE ups再添加一步。最后一个CTE在大多数情况下不会起任何作用。只有当返回的结果中缺少行时,我们才使用暴力。

还有更多的开销。与已存在的行冲突越多,这种方法就越有可能优于简单方法。

一个副作用是:第2个UPSERT按顺序写行,所以它重新引入了死锁的可能性(见下文),如果三个或更多事务写入相同的行重叠。如果这是一个问题,你需要一个不同的解决方案——比如重复上面提到的整个陈述。

并发问题2

如果并发事务可以写入受影响行的相关列,并且你必须确保你找到的行在同一事务的后面阶段仍然存在,你可以在CTE ins中使用锁定现有行(否则将被解锁):

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

并添加锁子句也是__ABC0的,比如FOR UPDATE

这使得相互竞争的写操作一直等到事务结束,也就是所有锁都被释放的时候。所以要简短。

详情及解释:

死锁?

通过在一致的顺序中插入行来防御死锁。看到的:

数据类型和类型转换

作为数据类型模板的现有表…

对独立VALUES表达式中的第一行数据进行显式类型转换可能不方便。有很多方法可以解决这个问题。您可以使用任何现有的关系(表、视图等)作为行模板。目标表是用例的明显选择。输入数据被自动强制转换为适当的类型,如INSERTVALUES子句:

WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
UNION ALL
VALUES
('foo1', 'bar1', 'bob1')  -- no type casts here
, ('foo2', 'bar2', 'bob2')
)
...

这对某些数据类型不起作用。看到的:

... 和名称

这也适用于所有数据类型。

在插入表的所有(前导)列时,可以省略列名。假设例子中的表chats仅由UPSERT中使用的3列组成:

WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*)         -- copies whole row definition
('foo1', 'bar1', 'bob1')  -- no type casts needed
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...

题外话:不要像"user"那样使用保留字作为标识符。那是一把上膛的脚枪。使用合法、小写、不带引号的标识符。我用usr替换它。

对于单个项的插入,我可能会在返回id时使用一个coalesce:

WITH new_chats AS (
INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
) SELECT COALESCE(
(SELECT id FROM new_chats),
(SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

对于多个项的插入,可以将值放在临时WITH中,并在以后引用它们:

WITH chats_values("user", "contact", "name") AS (
VALUES ($1, $2, $3),
($4, $5, $6)
), new_chats AS (
INSERT INTO chats ("user", "contact", "name")
SELECT * FROM chat_values
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
) SELECT id
FROM new_chats
UNION
SELECT chats.id
FROM chats, chats_values
WHERE chats.user = chats_values.user
AND chats.contact = chats_values.contact;

注意:根据Erwin的评论,在您的应用程序将尝试“upsert”相同的数据并发(两个worker试图同时插入<unique_field> = 1),并且这样的数据还不存在于表中,您应该在运行“upsert”之前更改事务的隔离级别:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

在这种特定情况下,两个事务中的一个将被中止。如果这种情况在您的应用程序中经常发生,您可能希望只执行2个单独的查询,否则,处理错误并重新执行查询会更容易、更快。

我修改了Erwin Brandstetter的惊人答案,它不会增加序列,也不会写锁任何行。我对PostgreSQL比较陌生,所以如果你看到这个方法的任何缺点,请随时告诉我:

WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, new_rows AS (
SELECT
c.usr
, c.contact
, c.name
, r.id IS NOT NULL as row_exists
FROM input_rows AS r
LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
)
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

这假设表chats对列(usr, contact)有唯一的约束。

更新:添加了spatar的建议修订(如下)。谢谢!

另一个更新,根据Revinand注释:

WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, new_rows AS (
INSERT INTO chats (usr, contact, name)
SELECT
c.usr
, c.contact
, c.name
FROM input_rows AS r
LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
WHERE r.id IS NULL
RETURNING id, usr, contact, name
)
SELECT id, usr, contact, name, 'new' as row_type
FROM new_rows
UNION ALL
SELECT id, usr, contact, name, 'update' as row_type
FROM input_rows AS ir
INNER JOIN chats AS c ON ir.usr=c.usr AND ir.contact=c.contact

我没有测试上面的内容,但如果你发现新插入的行被多次返回,那么你可以将UNION ALL更改为UNION,或者(更好),完全删除第一个查询。

WITH e AS(
INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3),
($2, $1, NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
)
SELECT * FROM e
UNION
SELECT id FROM chats WHERE user=$1, contact=$2;

使用ON CONFLICT DO NOTHING的主要目的是避免抛出错误,但它不会导致行返回。所以我们需要另一个SELECT来获取现有的id。

在此SQL中,如果它在冲突上失败,则它将不返回任何内容,然后第二个SELECT将获得现有行;如果它成功插入,那么将有两条相同的记录,然后我们需要UNION来合并结果。

最简单、最高效的解决方案是

BEGIN;


INSERT INTO chats ("user", contact, name)
VALUES ($1, $2, $3), ($2, $1, NULL)
ON CONFLICT ("user", contact) DO UPDATE
SET name = excluded.name
WHERE false
RETURNING id;


SELECT id
FROM chats
WHERE (user, contact) IN (($1, $2), ($2, $1));


COMMIT;

DO UPDATE WHERE false锁定但不更新该行,这是一个特性,而不是一个错误,因为它确保了另一个事务不能删除该行。

有些注释想要区分更新的行和创建的行。

在这种情况下,只需将txid_current() = xmin AS created添加到select。

根据Erwin上面的回答(顺便说一句,这是一个很棒的答案,如果没有它,我永远不会走到这里!),这就是我最终的结果。它解决了两个额外的潜在问题——它通过对输入集执行select distinct允许重复(否则会抛出错误),并且它确保返回的id与输入集完全匹配,包括相同的顺序并允许重复。

此外,对我来说很重要的一部分,它显著减少不必要的序列进度使用new_rows CTE只尝试插入那些还没有在那里。考虑到并发写入的可能性,它仍然会在这个简化集中遇到一些冲突,但后面的步骤将解决这个问题。在大多数情况下,序列间隙不是什么大问题,但当你做数十亿个upserts时,冲突的比例很高,它可能会导致ID使用int还是bigint的区别。

尽管它又大又丑,但性能非常好。我用数百万个upserts、高并发性和大量冲突对它进行了广泛测试。坚如磐石。

我将其打包为一个函数,但如果这不是您想要的,那么应该很容易看到如何将其转换为纯SQL。我还将示例数据更改为简单的内容。

CREATE TABLE foo
(
bar varchar PRIMARY KEY,
id  serial
);
CREATE TYPE ids_type AS (id integer);
CREATE TYPE bars_type AS (bar varchar);


CREATE OR REPLACE FUNCTION upsert_foobars(_vals bars_type[])
RETURNS SETOF ids_type AS
$$
BEGIN
RETURN QUERY
WITH
all_rows AS (
SELECT bar, ordinality
FROM UNNEST(_vals) WITH ORDINALITY
),
dist_rows AS (
SELECT DISTINCT bar
FROM all_rows
),
new_rows AS (
SELECT d.bar
FROM dist_rows d
LEFT JOIN foo f USING (bar)
WHERE f.bar IS NULL
),
ins AS (
INSERT INTO foo (bar)
SELECT bar
FROM new_rows
ORDER BY bar
ON CONFLICT DO NOTHING
RETURNING bar, id
),
sel AS (
SELECT bar, id
FROM ins
UNION ALL
SELECT f.bar, f.id
FROM dist_rows
JOIN foo f USING (bar)
),
ups AS (
INSERT INTO foo AS f (bar)
SELECT d.bar
FROM dist_rows d
LEFT JOIN sel s USING (bar)
WHERE s.bar IS NULL
ORDER BY bar
ON CONFLICT ON CONSTRAINT foo_pkey DO UPDATE
SET bar = f.bar
RETURNING bar, id
),
fin AS (
SELECT bar, id
FROM sel
UNION ALL
TABLE ups
)
SELECT f.id
FROM all_rows a
JOIN fin f USING (bar)
ORDER BY a.ordinality;
END
$$ LANGUAGE plpgsql;

如果您只想插入一行

然后你可以通过使用简单的EXISTS检查来显著地简化事情:

WITH
extant AS (
SELECT id FROM chats WHERE ("user", "contact") = ($1, $2)
),
inserted AS (
INSERT INTO chats ("user", "contact", "name")
SELECT $1, $2, $3
WHERE NOT EXISTS (SELECT NULL FROM extant)
RETURNING id
)
SELECT id FROM inserted
UNION ALL
SELECT id FROM extant

因为没有ON CONFLICT子句,所以没有更新——只有一个插入,而且只有在必要的时候。所以没有不必要的更新,没有不必要的写锁,没有不必要的序列增量。也不需要强制转换。

如果写锁是你用例中的一个特性,你可以在extant表达式中使用SELECT FOR UPDATE

如果你需要知道是否插入了新行,你可以在顶层UNION中添加一个标志列:

SELECT id, TRUE AS inserted FROM inserted
UNION ALL
SELECT id, FALSE FROM extant