带有 ORDER BY 的 Postgres UPDATE,怎么做?
Postgres UPDATE with ORDER BY, how to do it?
我需要对一组记录进行 Postgres 更新,并且我正在努力防止出现在压力测试中的死锁。
对此的典型解决方案是按特定顺序更新记录,例如按 ID - 但似乎 Postgres 不允许 ORDER BY 进行更新。
假设我需要进行更新,例如:
UPDATE BALANCES WHERE ID IN (SELECT ID FROM some_function() ORDER BY ID);
当您同时 运行 200 个查询时会导致死锁。怎么办?
我正在寻找一个通用的解决方案,而不是像 UPDATE with ORDER BY
中那样的针对特定案例的解决方法
感觉肯定有比编写游标函数更好的解决方案。另外,如果没有更好的方法,该游标功能的最佳外观如何?逐条更新
总的来说,并发比较难。特别是对于 200 条语句(我假设您不仅查询 = SELECT)甚至交易(实际上,如果发出的每条语句不在交易中,它都被包装到交易中)。
一般的解决方案概念是(组合)这些:
要注意可能会发生死锁,请在应用程序中捕获死锁,检查 Error Codes 是否有 class 40
或 40P01
并重试事务。
保留锁。使用 SELECT ... FOR UPDATE
。尽可能长时间地避免显式锁定。锁会强制其他事务等待锁释放,这会损害并发性,但可以防止事务 运行 陷入死锁。检查第 13 章中的死锁示例。尤其是事务 A 等待 B 和 B 等待 A(银行帐户)的那个。
如果可能,请选择不同的 Isolation Level,例如较弱的 READ COMMITED
。请注意 READ COMMITED
模式下的 LOST UPDATE
s。用 REPEATABLE READ
.
来阻止它们
在每个事务中以相同的顺序编写带有锁的语句,例如按 table 名称字母顺序。
LOCK / USE A -- Transaction 1
LOCK / USE B -- Transaction 1
LOCK / USE C -- Transaction 1
-- D not used -- Transaction 1
-- A not used -- Transaction 2
LOCK / USE B -- Transaction 2
-- C not used -- Transaction 2
LOCK / USE D -- Transaction 2
与一般锁定顺序 A B C D
。这样,事务可以以任何相对顺序交错,并且仍然有很好的机会不死锁(取决于您的语句,您可能还有其他序列化问题)。交易的语句将运行按照他们指定的顺序,但也可以是交易1运行是他们的第一个2,然后是xact 2运行是第一个,然后是1完成,最后 xact 2 完成。
此外,您应该意识到涉及多行的语句在并发情况下不会自动执行。换句话说,如果你有两条语句 A 和 B 涉及多行,那么它们可以按照这样的顺序执行:
a1 b1 a2 a3 a4 b2 b3
但不是作为 a 后跟 b 的块。
这同样适用于带有子查询的语句。
您是否使用 EXPLAIN
查看了查询计划?
你的情况,你可以试试
UPDATE BALANCES WHERE ID IN (
SELECT ID FROM some_function() FOR UPDATE -- LOCK using FOR UPDATE
-- other transactions will WAIT / BLOCK temporarily on conc. write access
);
如果可能的话,您还可以使用 SELECT ... FOR UPDATE SKIP LOCK,这将跳过已经锁定的数据以取回并发性,并发性因等待另一个事务释放锁而丢失(FOR UPDATE ).但这不会将 UPDATE 应用于锁定的行,而您的应用程序逻辑可能需要这样做。所以 运行 稍后(见第 1 点)。
另请阅读 LOST UPDATE 关于 LOST UPDATE
和
SKIP LOCKED 大约 SKIP LOCKED
。在您的情况下,队列可能是一个想法,SKIP LOCKED
参考中对此进行了完美解释,尽管关系 DBMS 并不意味着是队列。
HTH
据我所知,没有办法直接通过UPDATE
语句来实现;保证锁顺序的唯一方法是使用 SELECT ... ORDER BY ID 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
table 上重复 ID
索引查找的缺点。在您的简单示例中,您可以通过在锁定查询期间获取物理行地址(由 ctid
system column 表示)并使用它来驱动 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
s 时要小心,因为值是瞬态的。我们在这里很安全,因为锁会阻止任何更改。)
不幸的是,规划器只会在少数情况下使用 ctid
(您可以通过在 EXPLAIN
输出中查找 "Tid Scan" 节点来判断它是否有效) .在单个 UPDATE
语句中处理更复杂的查询,例如如果 some_function()
将您的新余额与 ID 一起返回,您需要退回到基于 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
$$
我需要对一组记录进行 Postgres 更新,并且我正在努力防止出现在压力测试中的死锁。
对此的典型解决方案是按特定顺序更新记录,例如按 ID - 但似乎 Postgres 不允许 ORDER BY 进行更新。
假设我需要进行更新,例如:
UPDATE BALANCES WHERE ID IN (SELECT ID FROM some_function() ORDER BY ID);
当您同时 运行 200 个查询时会导致死锁。怎么办?
我正在寻找一个通用的解决方案,而不是像 UPDATE with ORDER BY
中那样的针对特定案例的解决方法感觉肯定有比编写游标函数更好的解决方案。另外,如果没有更好的方法,该游标功能的最佳外观如何?逐条更新
总的来说,并发比较难。特别是对于 200 条语句(我假设您不仅查询 = SELECT)甚至交易(实际上,如果发出的每条语句不在交易中,它都被包装到交易中)。
一般的解决方案概念是(组合)这些:
要注意可能会发生死锁,请在应用程序中捕获死锁,检查 Error Codes 是否有
class 40
或40P01
并重试事务。保留锁。使用
SELECT ... FOR UPDATE
。尽可能长时间地避免显式锁定。锁会强制其他事务等待锁释放,这会损害并发性,但可以防止事务 运行 陷入死锁。检查第 13 章中的死锁示例。尤其是事务 A 等待 B 和 B 等待 A(银行帐户)的那个。如果可能,请选择不同的 Isolation Level,例如较弱的
READ COMMITED
。请注意READ COMMITED
模式下的LOST UPDATE
s。用REPEATABLE READ
. 来阻止它们
在每个事务中以相同的顺序编写带有锁的语句,例如按 table 名称字母顺序。
LOCK / USE A -- Transaction 1
LOCK / USE B -- Transaction 1
LOCK / USE C -- Transaction 1
-- D not used -- Transaction 1
-- A not used -- Transaction 2
LOCK / USE B -- Transaction 2
-- C not used -- Transaction 2
LOCK / USE D -- Transaction 2
与一般锁定顺序 A B C D
。这样,事务可以以任何相对顺序交错,并且仍然有很好的机会不死锁(取决于您的语句,您可能还有其他序列化问题)。交易的语句将运行按照他们指定的顺序,但也可以是交易1运行是他们的第一个2,然后是xact 2运行是第一个,然后是1完成,最后 xact 2 完成。
此外,您应该意识到涉及多行的语句在并发情况下不会自动执行。换句话说,如果你有两条语句 A 和 B 涉及多行,那么它们可以按照这样的顺序执行:
a1 b1 a2 a3 a4 b2 b3
但不是作为 a 后跟 b 的块。
这同样适用于带有子查询的语句。
您是否使用 EXPLAIN
查看了查询计划?
你的情况,你可以试试
UPDATE BALANCES WHERE ID IN (
SELECT ID FROM some_function() FOR UPDATE -- LOCK using FOR UPDATE
-- other transactions will WAIT / BLOCK temporarily on conc. write access
);
如果可能的话,您还可以使用 SELECT ... FOR UPDATE SKIP LOCK,这将跳过已经锁定的数据以取回并发性,并发性因等待另一个事务释放锁而丢失(FOR UPDATE ).但这不会将 UPDATE 应用于锁定的行,而您的应用程序逻辑可能需要这样做。所以 运行 稍后(见第 1 点)。
另请阅读 LOST UPDATE 关于 LOST UPDATE
和
SKIP LOCKED 大约 SKIP LOCKED
。在您的情况下,队列可能是一个想法,SKIP LOCKED
参考中对此进行了完美解释,尽管关系 DBMS 并不意味着是队列。
HTH
据我所知,没有办法直接通过UPDATE
语句来实现;保证锁顺序的唯一方法是使用 SELECT ... ORDER BY ID 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
table 上重复 ID
索引查找的缺点。在您的简单示例中,您可以通过在锁定查询期间获取物理行地址(由 ctid
system column 表示)并使用它来驱动 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
s 时要小心,因为值是瞬态的。我们在这里很安全,因为锁会阻止任何更改。)
不幸的是,规划器只会在少数情况下使用 ctid
(您可以通过在 EXPLAIN
输出中查找 "Tid Scan" 节点来判断它是否有效) .在单个 UPDATE
语句中处理更复杂的查询,例如如果 some_function()
将您的新余额与 ID 一起返回,您需要退回到基于 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
$$