我有下表:
mytable
-------
id
top
top_timestamp
我想更新插入
top
和 top_timestamp
,同时返回旧值(如果有)。如果该行已经存在,只有当我尝试插入的top
的值>当前值时,我才想更新它
该解决方案还必须考虑并发写入。
对于更新插入,到目前为止我有这个:
INSERT INTO mytable
AS mt (id, top, top_timestamp)
VALUES ('some-id', 123, 999)
ON CONFLICT (id)
DO UPDATE SET top=123, top_timestamp=999
WHERE mt.top < 123
我意识到
RETURNING
只能引用新值,因此要返回旧值,我有这个:
WITH old_values AS (SELECT top, top_timestamp FROM mytable WHERE id = 'some-id' FOR UPDATE),
update AS ($upsertSqlAbove)
SELECT top AS old_top, top_timestamp AS old_top_timestamp FROM old_values
这似乎可行,但是并发写入安全吗?
SELECT FOR UPDATE
是否按我的预期工作?也就是说,它会锁定该行直到整个查询完成?
第一个查询和这个有什么区别:
INSERT INTO mytable
AS mt (id, top, top_timestamp)
VALUES ('some-id', 123, 999)
ON CONFLICT (id)
DO UPDATE SET top=123, top_timestamp=999
WHERE mt.top < 123
RETURNING (SELECT top FROM mytable WHERE id = 123) AS last_top,
(SELECT top_timestamp FROM mytable WHERE id = 123) AS last_top_timestamp
第一个查询和这个有什么区别:
要么报告刚刚以并发安全方式更新的最后一行版本的旧值。但第一个更贵。它会提前锁定现有行。因此它持有锁的时间更长,并且可能会更长时间地阻止对同一行的并发写入。它还执行另外一条语句。而且,最重要的是,即使稍后没有发生
UPDATE
,它也会锁定该行,在这种情况下这完全是浪费。
所以第二个查询更好。除了两个缺陷:
它为每一列运行单独的
SELECT
。这似乎是必要的,因为 RETURNING
子句中的子查询表达式在这方面受到限制。
即使在
INSERT
之后,它也会运行这些 SELECT 查询。那么 SELECT
肯定是空的,但仍然是全额成本。
第三个查询修复了这两个缺陷:
INSERT INTO tbl AS t (id, top, top_timestamp)
VALUES ('some-id', 123, 999)
ON CONFLICT (id) DO UPDATE
SET top = EXCLUDED.top -- !
, top_timestamp = EXCLUDED.top_timestamp -- !
WHERE t.top < EXCLUDED.top -- !
RETURNING (SELECT t_old FROM tbl t_old
WHERE t_old.id = t.id
AND t.xmax <> 0 -- actually was an UPDATE!
).*
另外,使用特殊的 ROW 变量
EXCLUDED
。它保存建议插入的行,并有助于避免重复拼写输入值。 (细微差别:所有默认值和触发器都已应用。)