保证UPDATE FROM中的顺序,避免死锁

问题描述 投票:0回答:1

并发执行查询以原子方式增加一些具有聚合数值的计数器:

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
表可能非常大,并且该查询需要针对效率和完整性进行全面优化 - 有没有更好的方法来解决这个问题?

sql postgresql concurrency sql-update deadlock
1个回答
0
投票

可悲的是,

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
会立即删除目标中不匹配的输入行,如果有很多,这可能是一个显着的增益。

理论上这应该会快一点。你必须测试一下。

© www.soinside.com 2019 - 2024. All rights reserved.