据我所知,没有直接通过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
$$