zol*_*ola 127 sql postgresql upsert sql-returning
我在PostgreSQL 9.5中有以下UPSERT:
INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3),
($2, $1, NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;
Run Code Online (Sandbox Code Playgroud)
如果没有冲突,则返回如下内容:
----------
| id |
----------
1 | 50 |
----------
2 | 51 |
----------
Run Code Online (Sandbox Code Playgroud)
但如果存在冲突,则不会返回任何行:
----------
| id |
----------
Run Code Online (Sandbox Code Playgroud)
id
如果没有冲突,我想返回新列,或者返回id
冲突列的现有列.
可以这样做吗?如果是这样,怎么样?
Erw*_*ter 165
在目前接受的答案似乎确定了一些冲突,小元组和没有触发器.并且它通过暴力避免并发问题1(见下文).简单的解决方案有其吸引力,副作用可能不那么重要.
但是,对于所有其他情况,请勿在不需要的情况下更新相同的行.即使您在表面上看不到任何差异,也会产生各种副作用:
它可能触发不应该触发的触发器.
它写入锁定"无辜"行,可能会产生并发事务的成本.
它可能会使行看起来很新,虽然它很旧(事务时间戳).
最重要的是,使用PostgreSQL的MVCC模型,无论行数据是否相同,都会以任一方式编写新的行版本.这会导致UPSERT本身的性能损失,表膨胀,索引膨胀,表上所有后续操作的性能损失,VACUUM
成本.对于一些重复轻微的影响,但大规模的大部分受骗者.
没有空的更新和副作用,您可以(几乎)实现相同的效果.
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
Run Code Online (Sandbox Code Playgroud)
该ON CONFLICT DO UPDATE
列是一个可选的附加内容,用于演示其工作原理.实际上,您可能需要它来区分两种情况(另一种优于空写的优势).
最终的ON CONFLICT DO UPDATE
工作原理是因为附加的数据修改CTE中新插入的行在基础表中尚不可见.(同一SQL语句的所有部分都会看到基础表的相同快照.)
由于conflict_target
表达式是独立的(不直接附加到ON CONFLICT DO NOTHING
),Postgres无法从目标列派生数据类型,因此您可能必须添加显式类型转换.手册:
当
source
使用时JOIN chats
,值全部自动强制转换为相应目标列的数据类型.当它在其他上下文中使用时,可能需要指定正确的数据类型.如果条目都是引用的文字常量,则强制第一个足以确定所有的假定类型.
由于CTE和附加的开销(由于完美索引在定义中存在 - 使用索引实现了唯一约束),查询本身对于少数欺骗可能有点贵VALUES
.
许多重复可能(更快).额外写入的有效成本取决于许多因素.
但无论如何,副作用和隐藏成本都会减少.它总体上可能更便宜.
(附加序列仍然是高级的,因为在测试冲突之前会填写默认值.)
关于CTE:
假设默认INSERT
事务隔离.
关于dba.SE的相关答案,详细解释如下:
防范竞争条件的最佳策略取决于确切的要求,表格和UPSERT中行的数量和大小,并发交易的数量,冲突的可能性,可用资源和其他因素......
如果并发事务已写入您的事务现在尝试UPSERT的行,则您的事务必须等待另一个事务完成.
如果另一个交易以VALUES
(或任何错误,即自动INSERT
)结束,您的交易可以正常进行.副作用小:序号中的间隙.但没有丢失的行.
如果另一个事务正常结束(隐式或显式SELECT
),您READ COMMITTED
将检测到冲突(ROLLBACK
索引/约束是绝对的)ROLLBACK
,因此也不会返回该行.(也无法锁定行,如下面的并发问题2所示,因为它不可见.)COMMIT
从查询的开头看到相同的快照,也无法返回尚不可见的行.
结果集中缺少任何此类行(即使它们存在于基础表中)!
这可能是好的.特别是如果你没有像示例那样返回行,并且知道行存在就感到满意.如果这还不够好,可以采取各种方法.
您可以检查输出的行计数,如果它与输入的行计数不匹配,则重复该语句.对于罕见的情况可能足够好.关键是要启动一个新查询(可以在同一个事务中),然后查看新提交的行.
或者检查同一查询中是否缺少结果行,并用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;
Run Code Online (Sandbox Code Playgroud)
这就像上面的查询,但INSERT
在返回完整的结果集之前,我们再添加一个CTE步骤.最后一次CTE大部分时间都不会做任何事情.只有当返回的结果中缺少行时,我们才会使用暴力.
还有更多的开销.与预先存在的行冲突越多,它就越有可能胜过简单的方法.
一个副作用:第二个UPSERT不按顺序写行,所以如果写入相同行的三个或更多事务重叠,它会重新引入死锁的可能性(见下文).如果这是一个问题,您需要一个不同的解决方案.
如果并发事务可以写入受影响行的相关列,并且您必须确保在同一事务的稍后阶段仍然存在您找到的行,则可以使用以下方法以低成本方式锁定行:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
Run Code Online (Sandbox Code Playgroud)
并添加一个锁定子句UNIQUE
,如DO NOTHING
.
这使得竞争写入操作等到事务结束时,所有锁定都被释放.所以要简短一点.
更多细节和解释:
通过以一致的顺序插入行来防御死锁.看到:
对于独立SELECT
表达式中的第一行数据的显式类型转换可能是不方便的.有办法解决它.您可以使用任何现有关系(表,视图,...)作为行模板.目标表是用例的明显选择.输入数据被自动强制转换为适当的类型,如在一个ups
的条款ins
:
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')
)
...
Run Code Online (Sandbox Code Playgroud)
这对某些数据类型不起作用(在底部的链接答案中有解释).下一个技巧适用于所有数据类型:
如果插入整行(表的所有列 - 或至少一组前导列),也可以省略列名.假设SELECT
示例中的表只使用了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
)
...
Run Code Online (Sandbox Code Playgroud)
详细解释和更多替代方案:
旁白:不要使用像FOR UPDATE
标识符这样的保留字.那是一个装满脚的枪.使用合法的,小写的,不带引号的标识符.我换了它VALUES
.
小智 73
我有完全相同的问题,我使用'do update'而不是'什么都不做'来解决它,即使我没有任何更新.在你的情况下,它将是这样的:
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;
Run Code Online (Sandbox Code Playgroud)
此查询将返回所有行,无论它们刚刚插入还是之前存在.
Jau*_*era 15
作为INSERT
查询的扩展,Upsert 可以在约束冲突的情况下使用两种不同的行为来定义:DO NOTHING
或DO UPDATE
.
INSERT INTO upsert_table VALUES (2, 6, 'upserted')
ON CONFLICT DO NOTHING RETURNING *;
id | sub_id | status
----+--------+--------
(0 rows)
Run Code Online (Sandbox Code Playgroud)
请注意,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)
Run Code Online (Sandbox Code Playgroud)
Yu *_*ang 14
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;
Run Code Online (Sandbox Code Playgroud)
使用的主要目的ON CONFLICT DO NOTHING
是避免抛出错误,但会导致无行返回。所以我们需要另一个SELECT
来获取现有的 id。
在这个 SQL 中,如果冲突失败,它将不返回任何内容,然后第二个SELECT
将获取现有行;如果插入成功,那么就会有两条相同的记录,这时我们需要UNION
合并结果。
Joã*_*aas 13
对于单个项目的插入,我可能会在返回 id 时使用合并:
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)
);
Run Code Online (Sandbox Code Playgroud)
对于插入多个项目,您可以将值放在一个临时的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
Run Code Online (Sandbox Code Playgroud)
Ari*_*zis 11
然后,您可以通过使用简单的检查来显着简化事情EXISTS
:
WITH\n extant AS (\n SELECT id FROM chats WHERE ("user", "contact") = ($1, $2)\n ),\n inserted AS (\n INSERT INTO chats ("user", "contact", "name")\n SELECT $1, $2, $3\n WHERE NOT EXISTS (SELECT FROM extant)\n RETURNING id\n )\nSELECT id FROM inserted\nUNION ALL\nSELECT id FROM extant\n
Run Code Online (Sandbox Code Playgroud)\n由于没有ON CONFLICT
子句,因此没有更新 \xe2\x80\x93 只有插入,并且仅在必要时进行。因此,没有不必要的更新,没有不必要的写锁,没有不必要的序列增量。也不需要演员表。
如果写锁是您用例中的一项功能,则可以SELECT FOR UPDATE
在extant
表达式中使用。
如果您需要知道是否插入了新行,您可以在顶层添加一个标志列UNION
:
SELECT id, TRUE AS inserted FROM inserted\nUNION ALL\nSELECT id, FALSE FROM extant\n
Run Code Online (Sandbox Code Playgroud)\n
以上面 Erwin 的回答为基础(顺便说一句,很棒的回答,如果没有它,永远不会到达这里!),这就是我结束的地方。它解决了一些额外的潜在问题 - 它通过对输入集执行 a允许重复(否则会引发错误)select distinct
,并确保返回的 ID 与输入集完全匹配,包括相同的顺序并允许重复。
此外,还有一个对我来说很重要的部分,它使用CTE显着减少了不必要的序列改进的数量,new_rows
只尝试插入那些不在那里的序列。考虑到并发写入的可能性,它仍然会在减少的集合中遇到一些冲突,但后面的步骤会解决这个问题。在大多数情况下,序列间隙并不是什么大问题,但是当您进行数十亿次 upsert 且冲突比例很高时,这可能会导致对 ID使用 anint
或 a的区别bigint
。
尽管又大又丑,它的表现却非常出色。我用数百万个 upsert、高并发、大量冲突对它进行了广泛的测试。坚如磐石。
我已将其打包为一个函数,但如果这不是您想要的,那么应该很容易了解如何转换为纯 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;
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
44914 次 |
最近记录: |