据我所知,没有直接通过
UPDATE
语句实现这一点的方法;确保锁定顺序的唯一方法是使用
SELECT ... ORDER BY ID FOR UPDATE
明确地获取锁,例如:
SELECT * FROM mytable WHERE id = 1 FOR UPDATE;
UPDATE Balances
SET Balance = 0
WHERE ID IN (
SELECT ID FROM Balances
WHERE ID IN (SELECT ID FROM some_function())
ORDER BY ID
FOR UPDATE
)
这样做的缺点是在Balances
表上重复查找ID
索引。在您的简单示例中,您可以通过在锁定查询期间获取物理行地址(由ctid
系统列表示)来避免此开销,并使用它来驱动UPDATE
:
UPDATE Balances
SET Balance = 0
WHERE ctid = ANY(ARRAY(
SELECT ctid FROM Balances
WHERE ID IN (SELECT ID FROM some_function())
ORDER BY ID
FOR UPDATE
))
使用ctid
时要小心,因为该值是瞬时的。我们在这里是安全的,因为锁将阻止任何更改。
不幸的是,规划器只在一组狭窄的情况下利用ctid
(您可以通过查看EXPLAIN
输出中的"Tid Scan"节点来确定它是否有效)。如果要在单个UPDATE
语句中处理更复杂的查询,例如如果您的新余额与ID一起由some_function()
返回,则需要回退到基于ID的查找:
UPDATE Balances
SET Balance = Locks.NewBalance
FROM (
SELECT Balances.ID, some_function.NewBalance
FROM Balances
JOIN some_function() ON some_function.ID = Balances.ID
ORDER BY Balances.ID
FOR UPDATE
) Locks
WHERE Balances.ID = Locks.ID
如果性能开销是一个问题,你需要使用游标来解决,具体代码如下:
DO $$
DECLARE
c CURSOR FOR
SELECT Balances.ID, some_function.NewBalance
FROM Balances
JOIN some_function() ON some_function.ID = Balances.ID
ORDER BY Balances.ID
FOR UPDATE;
BEGIN
FOR row IN c LOOP
UPDATE Balances
SET Balance = row.NewBalance
WHERE CURRENT OF c;
END LOOP;
END
$$