如何在 PostgreSQL 中使用 RETURNING with ON CONFLICT?
How to use RETURNING with ON CONFLICT in PostgreSQL?
我在 PostgreSQL 9.5 中有以下 UPSERT:
INSERT INTO chats ("user", "contact", "name")
VALUES (, , ),
(, , NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;
如果没有冲突,return是这样的:
----------
| id |
----------
1 | 50 |
----------
2 | 51 |
----------
但如果有冲突,它不会 return 任何行:
----------
| id |
----------
如果没有冲突,我想 return 新的 id
列,或者 return 冲突列的现有 id
列。
这可以做到吗?如果可以,怎么做?
Upsert,作为 INSERT
查询的扩展,在约束冲突的情况下可以定义两种不同的行为:DO NOTHING
或 DO UPDATE
.
INSERT INTO upsert_table VALUES (2, 6, 'upserted')
ON CONFLICT DO NOTHING RETURNING *;
id | sub_id | status
----+--------+--------
(0 rows)
还要注意 RETURNING
returns 什么都没有,因为没有插入元组。现在使用 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)
我遇到了完全相同的问题,我使用 'do update' 而不是 'do nothing' 解决了它,尽管我没有什么可更新的。在您的情况下,它将是这样的:
INSERT INTO chats ("user", "contact", "name")
VALUES (, , ),
(, , NULL)
ON CONFLICT("user", "contact")
DO UPDATE SET
name=EXCLUDED.name
RETURNING id;
此查询将 return 所有行,无论它们是刚刚插入的还是之前存在的。
对于单个冲突目标、很少的冲突、小元组和没有触发器似乎没问题。它通过暴力避免了 并发问题 1(见下文)。简单的解决方案有其吸引力,副作用可能不那么重要。
但是,对于所有其他情况,而不是 不需要更新相同的行。即使你表面上看不出有什么不同,但也有各种副作用:
它可能会触发不应触发的触发器。
它 write-locks “无害”行,可能会产生并发事务成本。
它可能会使该行看起来很新,尽管它很旧(事务时间戳)。
最重要的是,对于PostgreSQL's MVCC model,无论行数据是否改变,都会为每个UPDATE
写入一个新的行版本.这会导致 UPSERT 本身的性能损失,table 膨胀,索引膨胀,后续操作的性能损失 table,VACUUM
成本。对少数重复的影响很小,但大量对大多数受骗的影响。
加上,有时用ON CONFLICT DO UPDATE
不实用甚至不可能。 The manual:
For ON CONFLICT DO UPDATE
, a conflict_target
must be provided.
如果涉及多个索引/约束,单个“冲突目标”是不可能的。但是这里有一个针对多个部分索引的相关解决方案:
回到主题,您可以(几乎)实现相同的效果而没有空更新和副作用。以下一些解决方案也适用于 ON CONFLICT DO NOTHING
(无“冲突目标”),以捕获可能出现的 所有 可能发生的冲突 - 这可能是可取的,也可能不是可取的。
没有并发写入负载
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 的新插入行在基础 table 中尚不可见。 (同一 SQL 语句的所有部分都看到底层 table 的相同快照。)
由于 VALUES
表达式是 free-standing(不直接附加到 INSERT
),Postgres 无法从目标列派生数据类型,您可能必须添加显式类型转换。 The manual:
When VALUES
is used in INSERT
, the values are all automatically
coerced to the data type of the corresponding destination column. When
it's used in other contexts, it might be necessary to specify the
correct data type. If the entries are all quoted literal constants,
coercing the first is sufficient to determine the assumed type for all.
由于 CTE 的开销和额外的 SELECT
,查询本身(不计算副作用)对于 少数 欺骗来说可能有点昂贵(这应该很便宜,因为根据定义存在完美索引 - 唯一约束是用索引实现的)。
对于许多 次重复可能(多)快。额外写入的有效成本取决于许多因素。
但在任何情况下,副作用和隐性成本都较少。整体上很可能更便宜。
附加序列仍然是高级的,因为在测试冲突之前填充了默认值。
关于 CTE:
- Are SELECT type queries the only type that can be nested?
- Deduplicate SELECT statements in relational division
并发写入负载
假设默认 READ COMMITTED
transaction isolation。相关:
防止竞争条件的最佳策略取决于确切的要求、table 和 UPSERT 中行的数量和大小、并发事务的数量、冲突的可能性、可用资源和其他因素...
并发问题 1
如果并发事务已写入您的事务现在尝试 UPSERT 的行,则您的事务必须等待另一个事务完成。
如果其他交易以ROLLBACK
结束(或任何错误,即自动ROLLBACK
),您的交易可以正常进行。可能的次要副作用:序列号中的间隙。但是没有丢失的行。
如果其他事务正常结束(隐式或显式 COMMIT
),您的 INSERT
将检测到冲突(UNIQUE
索引/约束是绝对的)并且 DO NOTHING
,因此也不是 return 该行。 (也无法锁定该行,如下面 并发问题 2 所示,因为它 不可见 。) SELECT
看到相同的快照从查询开始,也不能 return 还不可见的行。
结果集中缺少任何此类行(即使它们存在于底层 table)!
这个可能没问题。特别是如果您不像示例中那样 returning 行并且知道该行在那里就很满意。如果这还不够好,还有多种解决方法。
您可以检查输出的行数,如果与输入的行数不匹配,则重复该语句。可能很好enogh 对于极少数情况。重点是开始一个新查询(可以在同一个事务中),然后它将看到新提交的行。
或检查丢失的结果行在相同的查询中覆盖 .
中演示的蛮力技巧
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;
就像上面的查询,但是我们在 return complete 之前用 CTE ups
多了一步 结果集。最后一个 CTE 大多数时候什么都不做。仅当行从 returned 结果中丢失时,我们才使用蛮力。
还有更多开销。与 pre-existing 行的冲突越多,这种方法越有可能胜过简单方法。
一个副作用:第二个 UPSERT 乱序写入行,因此如果 三个或更多 个事务写入到 re-introduces 死锁的可能性(见下文)相同的行重叠。如果这是一个问题,您需要一个不同的解决方案 - 比如重复上面提到的整个语句。
并发问题 2
如果并发事务可以写入受影响行的相关列,并且您必须确保您找到的行在同一事务的稍后阶段仍然存在,您可以锁定现有行 在 CTE ins
中便宜(否则会解锁):
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
并添加一个locking clause to the SELECT
as well, like FOR UPDATE
.
这使得竞争写入操作一直等到事务结束,此时所有锁都被释放。所以要简短。
更多细节和解释:
- Is SELECT or INSERT in a function prone to race conditions?
死锁?
通过以一致的顺序插入行来防止死锁。参见:
数据类型和转换
现有 table 作为数据类型的模板...
free-standing VALUES
表达式中第一行数据的显式类型转换可能不方便。有很多方法可以解决。您可以使用任何现有关系(table、视图、...)作为行模板。目标 table 是用例的明显选择。输入数据被自动强制转换为适当的类型,例如 INSERT
:
的 VALUES
子句
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')
)
...
这不适用于某些数据类型。参见:
- Casting NULL type when updating multiple rows
...和名字
这也适用于所有 数据类型。
在插入 table 的所有(前导)列时,您可以省略列名。假设示例中的 table 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"
一样使用 reserved words 作为标识符。那是一把上膛的脚枪。使用合法的 lower-case,不带引号的标识符。我用 usr
.
替换了它
对于单个项目的插入,我可能会在返回 id 时使用合并:
WITH new_chats AS (
INSERT INTO chats ("user", "contact", "name")
VALUES (, , )
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
) SELECT COALESCE(
(SELECT id FROM new_chats),
(SELECT id FROM chats WHERE user = AND contact = )
);
对于多个项目的插入,您可以将值放在一个临时的 WITH
上并稍后引用它们:
WITH chats_values("user", "contact", "name") AS (
VALUES (, , ),
(, , )
), 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' 相同的数据(两个工作人员试图插入 <unique_field> = 1
同时),并且 table 上不存在此类数据,您应该在 运行 之前更改事务的隔离级别 '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
这假定 table 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 (, , ),
(, , NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
)
SELECT * FROM e
UNION
SELECT id FROM chats WHERE user=, contact=;
使用ON CONFLICT DO NOTHING
的主要目的是为了避免抛出错误,但是会导致没有行return。所以我们需要另一个SELECT
来获取现有的id。
在这个 SQL 中,如果它因冲突而失败,它将 return 什么都没有,然后第二个 SELECT
将获取现有行;如果插入成功,那么会有两条相同的记录,那么我们需要UNION
合并结果。
最简单、最高效的解决方案是
BEGIN;
INSERT INTO chats ("user", contact, name)
VALUES (, , ), (, , NULL)
ON CONFLICT ("user", contact) DO UPDATE
SET name = excluded.name
WHERE false
RETURNING id;
SELECT id
FROM chats
WHERE (user, contact) IN ((, ), (, ));
COMMIT;
DO UPDATE WHERE false
锁定但不更新行,这是一个功能,而不是错误,因为它确保另一个事务无法删除该行。
一些评论想要区分更新的行和创建的行。
在这种情况下,只需将 txid_current() = xmin AS created
添加到 select。
基于上面 Erwin 的回答(顺便说一句,非常棒的回答,没有它我永远不会得到这里!),这就是我的结局。它解决了一些额外的潜在问题 - 它 通过对输入集执行 select distinct
允许重复 (否则会引发错误),并且它 确保返回的 ID 与输入集完全匹配,包括相同的顺序并允许重复。
此外,对我来说很重要的一个部分是,它 显着减少了不必要的序列推进的数量 使用 new_rows
CTE 仅尝试插入那些还没有在那里。考虑到并发写入的可能性,它仍然会在该缩减集中遇到一些冲突,但后面的步骤会解决这个问题。在大多数情况下,序列间隙不是什么大问题,但是当您进行数十亿次更新插入时,冲突比例很高,使用 int
或 bigint
可能会有所不同求身份证。
尽管又大又丑,但它的性能非常好。我用数百万次更新、高并发、大量冲突对其进行了广泛测试。坚如磐石。
我已将它打包为一个函数,但如果这不是您想要的,应该很容易看出如何转换为纯函数 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") = (, )
),
inserted AS (
INSERT INTO chats ("user", "contact", "name")
SELECT , ,
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
我在 PostgreSQL 9.5 中有以下 UPSERT:
INSERT INTO chats ("user", "contact", "name")
VALUES (, , ),
(, , NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;
如果没有冲突,return是这样的:
----------
| id |
----------
1 | 50 |
----------
2 | 51 |
----------
但如果有冲突,它不会 return 任何行:
----------
| id |
----------
如果没有冲突,我想 return 新的 id
列,或者 return 冲突列的现有 id
列。
这可以做到吗?如果可以,怎么做?
Upsert,作为 INSERT
查询的扩展,在约束冲突的情况下可以定义两种不同的行为:DO NOTHING
或 DO UPDATE
.
INSERT INTO upsert_table VALUES (2, 6, 'upserted')
ON CONFLICT DO NOTHING RETURNING *;
id | sub_id | status
----+--------+--------
(0 rows)
还要注意 RETURNING
returns 什么都没有,因为没有插入元组。现在使用 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)
我遇到了完全相同的问题,我使用 'do update' 而不是 'do nothing' 解决了它,尽管我没有什么可更新的。在您的情况下,它将是这样的:
INSERT INTO chats ("user", "contact", "name")
VALUES (, , ),
(, , NULL)
ON CONFLICT("user", "contact")
DO UPDATE SET
name=EXCLUDED.name
RETURNING id;
此查询将 return 所有行,无论它们是刚刚插入的还是之前存在的。
但是,对于所有其他情况,而不是 不需要更新相同的行。即使你表面上看不出有什么不同,但也有各种副作用:
它可能会触发不应触发的触发器。
它 write-locks “无害”行,可能会产生并发事务成本。
它可能会使该行看起来很新,尽管它很旧(事务时间戳)。
最重要的是,对于PostgreSQL's MVCC model,无论行数据是否改变,都会为每个
UPDATE
写入一个新的行版本.这会导致 UPSERT 本身的性能损失,table 膨胀,索引膨胀,后续操作的性能损失 table,VACUUM
成本。对少数重复的影响很小,但大量对大多数受骗的影响。
加上,有时用ON CONFLICT DO UPDATE
不实用甚至不可能。 The manual:
For
ON CONFLICT DO UPDATE
, aconflict_target
must be provided.
如果涉及多个索引/约束,单个“冲突目标”是不可能的。但是这里有一个针对多个部分索引的相关解决方案:
回到主题,您可以(几乎)实现相同的效果而没有空更新和副作用。以下一些解决方案也适用于 ON CONFLICT DO NOTHING
(无“冲突目标”),以捕获可能出现的 所有 可能发生的冲突 - 这可能是可取的,也可能不是可取的。
没有并发写入负载
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 的新插入行在基础 table 中尚不可见。 (同一 SQL 语句的所有部分都看到底层 table 的相同快照。)
由于 VALUES
表达式是 free-standing(不直接附加到 INSERT
),Postgres 无法从目标列派生数据类型,您可能必须添加显式类型转换。 The manual:
When
VALUES
is used inINSERT
, the values are all automatically coerced to the data type of the corresponding destination column. When it's used in other contexts, it might be necessary to specify the correct data type. If the entries are all quoted literal constants, coercing the first is sufficient to determine the assumed type for all.
由于 CTE 的开销和额外的 SELECT
,查询本身(不计算副作用)对于 少数 欺骗来说可能有点昂贵(这应该很便宜,因为根据定义存在完美索引 - 唯一约束是用索引实现的)。
对于许多 次重复可能(多)快。额外写入的有效成本取决于许多因素。
但在任何情况下,副作用和隐性成本都较少。整体上很可能更便宜。
附加序列仍然是高级的,因为在测试冲突之前填充了默认值。
关于 CTE:
- Are SELECT type queries the only type that can be nested?
- Deduplicate SELECT statements in relational division
并发写入负载
假设默认 READ COMMITTED
transaction isolation。相关:
防止竞争条件的最佳策略取决于确切的要求、table 和 UPSERT 中行的数量和大小、并发事务的数量、冲突的可能性、可用资源和其他因素...
并发问题 1
如果并发事务已写入您的事务现在尝试 UPSERT 的行,则您的事务必须等待另一个事务完成。
如果其他交易以ROLLBACK
结束(或任何错误,即自动ROLLBACK
),您的交易可以正常进行。可能的次要副作用:序列号中的间隙。但是没有丢失的行。
如果其他事务正常结束(隐式或显式 COMMIT
),您的 INSERT
将检测到冲突(UNIQUE
索引/约束是绝对的)并且 DO NOTHING
,因此也不是 return 该行。 (也无法锁定该行,如下面 并发问题 2 所示,因为它 不可见 。) SELECT
看到相同的快照从查询开始,也不能 return 还不可见的行。
结果集中缺少任何此类行(即使它们存在于底层 table)!
这个可能没问题。特别是如果您不像示例中那样 returning 行并且知道该行在那里就很满意。如果这还不够好,还有多种解决方法。
您可以检查输出的行数,如果与输入的行数不匹配,则重复该语句。可能很好enogh 对于极少数情况。重点是开始一个新查询(可以在同一个事务中),然后它将看到新提交的行。
或检查丢失的结果行在相同的查询中覆盖
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;
就像上面的查询,但是我们在 return complete 之前用 CTE ups
多了一步 结果集。最后一个 CTE 大多数时候什么都不做。仅当行从 returned 结果中丢失时,我们才使用蛮力。
还有更多开销。与 pre-existing 行的冲突越多,这种方法越有可能胜过简单方法。
一个副作用:第二个 UPSERT 乱序写入行,因此如果 三个或更多 个事务写入到 re-introduces 死锁的可能性(见下文)相同的行重叠。如果这是一个问题,您需要一个不同的解决方案 - 比如重复上面提到的整个语句。
并发问题 2
如果并发事务可以写入受影响行的相关列,并且您必须确保您找到的行在同一事务的稍后阶段仍然存在,您可以锁定现有行 在 CTE ins
中便宜(否则会解锁):
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
并添加一个locking clause to the SELECT
as well, like FOR UPDATE
.
这使得竞争写入操作一直等到事务结束,此时所有锁都被释放。所以要简短。
更多细节和解释:
- Is SELECT or INSERT in a function prone to race conditions?
死锁?
通过以一致的顺序插入行来防止死锁。参见:
数据类型和转换
现有 table 作为数据类型的模板...
free-standing VALUES
表达式中第一行数据的显式类型转换可能不方便。有很多方法可以解决。您可以使用任何现有关系(table、视图、...)作为行模板。目标 table 是用例的明显选择。输入数据被自动强制转换为适当的类型,例如 INSERT
:
VALUES
子句
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')
)
...
这不适用于某些数据类型。参见:
- Casting NULL type when updating multiple rows
...和名字
这也适用于所有 数据类型。
在插入 table 的所有(前导)列时,您可以省略列名。假设示例中的 table 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"
一样使用 reserved words 作为标识符。那是一把上膛的脚枪。使用合法的 lower-case,不带引号的标识符。我用 usr
.
对于单个项目的插入,我可能会在返回 id 时使用合并:
WITH new_chats AS (
INSERT INTO chats ("user", "contact", "name")
VALUES (, , )
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
) SELECT COALESCE(
(SELECT id FROM new_chats),
(SELECT id FROM chats WHERE user = AND contact = )
);
对于多个项目的插入,您可以将值放在一个临时的 WITH
上并稍后引用它们:
WITH chats_values("user", "contact", "name") AS (
VALUES (, , ),
(, , )
), 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' 相同的数据(两个工作人员试图插入 <unique_field> = 1
同时),并且 table 上不存在此类数据,您应该在 运行 之前更改事务的隔离级别 '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
这假定 table 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 (, , ),
(, , NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id
)
SELECT * FROM e
UNION
SELECT id FROM chats WHERE user=, contact=;
使用ON CONFLICT DO NOTHING
的主要目的是为了避免抛出错误,但是会导致没有行return。所以我们需要另一个SELECT
来获取现有的id。
在这个 SQL 中,如果它因冲突而失败,它将 return 什么都没有,然后第二个 SELECT
将获取现有行;如果插入成功,那么会有两条相同的记录,那么我们需要UNION
合并结果。
最简单、最高效的解决方案是
BEGIN;
INSERT INTO chats ("user", contact, name)
VALUES (, , ), (, , NULL)
ON CONFLICT ("user", contact) DO UPDATE
SET name = excluded.name
WHERE false
RETURNING id;
SELECT id
FROM chats
WHERE (user, contact) IN ((, ), (, ));
COMMIT;
DO UPDATE WHERE false
锁定但不更新行,这是一个功能,而不是错误,因为它确保另一个事务无法删除该行。
一些评论想要区分更新的行和创建的行。
在这种情况下,只需将 txid_current() = xmin AS created
添加到 select。
基于上面 Erwin 的回答(顺便说一句,非常棒的回答,没有它我永远不会得到这里!),这就是我的结局。它解决了一些额外的潜在问题 - 它 通过对输入集执行 select distinct
允许重复 (否则会引发错误),并且它 确保返回的 ID 与输入集完全匹配,包括相同的顺序并允许重复。
此外,对我来说很重要的一个部分是,它 显着减少了不必要的序列推进的数量 使用 new_rows
CTE 仅尝试插入那些还没有在那里。考虑到并发写入的可能性,它仍然会在该缩减集中遇到一些冲突,但后面的步骤会解决这个问题。在大多数情况下,序列间隙不是什么大问题,但是当您进行数十亿次更新插入时,冲突比例很高,使用 int
或 bigint
可能会有所不同求身份证。
尽管又大又丑,但它的性能非常好。我用数百万次更新、高并发、大量冲突对其进行了广泛测试。坚如磐石。
我已将它打包为一个函数,但如果这不是您想要的,应该很容易看出如何转换为纯函数 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") = (, )
),
inserted AS (
INSERT INTO chats ("user", "contact", "name")
SELECT , ,
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