并发执行查询以原子方式增加一些具有聚合数值的计数器:
WITH data (
id,
delta
) AS (
VALUES(1,1.2),(2,2.0),(2,0.5)
), agg_data AS (
SELECT
id::bigint,
SUM(delta::numeric) AS sum_delta
FROM
data
GROUP BY
id
ORDER BY
id
)
UPDATE
counters
SET
val = counters.val + agg_data.sum_delta
FROM
agg_data
WHERE
counters.id = agg_data.id;
设置了 CTE
id
中 agg_data
列的顺序,但 UPDATE 语句不遵循该顺序,而是以任意顺序更新 counters
中的行,这会导致死锁。不幸的是,UPDATE 不支持 ORDER BY(想知道是否有原因)。
虽然我可以添加一个子查询,这会
SELECT FOR UPDATE
,但感觉我把事情变得过于复杂了:
WITH data (
id,
delta
) AS (
VALUES(1,1.2),(2,2.0),(2,0.5)
), agg_data AS (
SELECT
id::bigint,
SUM(delta::numeric) AS sum_delta
FROM
data
GROUP BY
id
ORDER BY
id
), locked_counters AS ( -- add this part to lock in sequential order
SELECT
FROM
counters
WHERE
id
IN (SELECT id FROM agg_data)
ORDER BY
id
FOR NO KEY UPDATE)
UPDATE
counters
SET
val = counters.val + agg_data.sum_delta
FROM
agg_data
WHERE
counters.id = agg_data.id;
考虑到我的
counters
表可能非常大,并且该查询需要针对效率和完整性进行全面优化 - 有没有更好的方法来解决这个问题?
可悲的是,
ORDER BY
没有UPDATE
。我们必须使用 SELECT FOR UPDATE
系列工具之一采取显式行级锁定。在单独的 CTE 中的 FOR NO KEY UPDATE
之后的
ORDER BY
似乎是最好的选择。您走在正确的道路上。
自从...
此查询需要针对效率和完整性进行全面优化
我有几个建议:
WITH data (id, delta) AS (
VALUES (null::bigint, null::numeric) -- ①
UNION ALL
VALUES (1,1.2), (2,2.0), (2,0.5)
)
, agg_data AS ( -- ②
SELECT d.*
FROM (
SELECT id, sum(delta) AS sum_delta
FROM data
GROUP BY id
ORDER BY id -- optional
) d
JOIN counters USING (id) -- ③
ORDER BY id
FOR NO KEY UPDATE OF counters -- ②
)
UPDATE counters c
SET val = c.val + a.sum_delta
FROM agg_data a
WHERE c.id = a.id;
① 不要让输入值默认为某种数据类型,只是为了稍后进行转换。这是浪费精力(可能会引入舍入误差)。相反,立即将输入转换为正确的类型。您可以通过转换
VALUES
表达式中第一行(或任何一行)的值来做到这一点:
VALUES (bigint '1', numeric '1.2'), (2,2.0), (2,0.5)
这可能会很不方便,因为它会强制您编辑输入。下一个最好的事情就是像我上面做的那样。然后你就有了一个不可变的、前导行的正确数据类型的
null
值,迫使集合的其余部分保持一致。虚拟行将在后续连接中自动消除。详情请参阅:
② 您有一个 CTE
agg_data
来具体化聚合结果,还有另一个 CTE locked_counters
来按顺序获取行锁。您可以在单个 CTE 中完成这两项操作。但是,引用手册:
目前,
、FOR NO KEY UPDATE
、FOR UPDATE
和FOR SHARE
不能用FOR KEY SHARE
指定。GROUP BY
Postgres 在意识到锁无论如何不适用于该派生表之前会引发异常。一个缺点,但我们可以用
FOR NO KEY UPDATE OF counters
来解决它。
③
JOIN
通常比 IN (SELECT ...)
更快。 IN
尝试折叠子查询中的重复项。在 GROUP BY id
之后,不可能有任何欺骗,但 Postgres 不会知道,并且仍然遵循更昂贵的代码路径。
此外,JOIN
会立即删除目标中不匹配的输入行,如果有很多,这可能是一个显着的增益。
理论上这应该会快一点。你必须测试一下。