是的。我认为这里的主要问题是IN 检查指定集合中的成员资格,但没有对UPDATE 赋予任何排序,这反过来意味着没有赋予锁定排序的具体排序。
UPDATE 语句中的WHERE 子句本质上与SELECT 中的行为方式相同。例如,我会经常使用SELECT 模拟UPDATE,以检查将更新的内容是否符合我的预期。
考虑到这一点,以下使用SELECT 的示例表明IN 本身并不赋予排序:
鉴于此架构/数据:
create table foo
(
id serial,
val text
);
insert into foo (val)
values ('one'), ('two'), ('three'), ('four');
以下查询:
select *
from foo
where id in (1,2,3,4);
select *
from foo
where id in (4,3,2,1);
产生完全相同的结果——行从id 1-4 开始。
即使这不是保证,因为我没有在选择中使用ORDER BY。相反,如果没有它,Postgres 会使用服务器决定最快的任何顺序(参见 Postgres SELECT 文档中关于 ORDER BY 的第 8 点)。给定一个相当静态的表,它通常与插入的顺序相同(就像这里的情况一样)。但是,并不能保证这一点,而且如果表中存在大量流失(大量死元组、删除的行等),则不太可能出现这种情况。
我怀疑这就是您的UPDATE 所发生的情况。有时——即使不是大部分时间——如果这与插入行的方式相同,它可能会以数字顺序结束,但没有什么可以保证,并且您看到死锁的情况可能是数据的情况已更改,使得一个更新的顺序与另一个不同。
sqlfiddle 上面的代码。
可能的修复/解决方法:
就您可以采取的措施而言,有多种选择,具体取决于您的要求。您可以在表上显式取出表锁,尽管这当然会产生序列化更新的效果,这可能会成为太大的瓶颈。
另一个仍然允许并发的选项是使用动态 SQL 在例如 Python 中显式迭代项目。这样,您将拥有一组始终以相同顺序发生的单行更新,并且由于您可以确保一致的顺序,正常的 Postgres 锁定应该能够处理并发,而无需死锁。
这不会像纯 SQL 中的批量更新那样执行,但它应该可以解决锁定问题。提高性能的一个建议是每隔一段时间只使用COMMIT,而不是在每一行之后——这样可以节省大量开销。
另一种选择是在 PL/pgSQL 中编写的 Postgres 函数中执行循环。然后可以在外部调用该函数,例如在 Python 中,但循环将在服务器端(也明确地)完成,这可能会节省一些开销,因为循环和 UPDATEs 是完全在服务器端完成,无需在每次循环迭代中走线。