cpp*_*ner 14 sql postgresql concurrency common-table-expression
我们的生产系统遇到了一个非常奇怪的问题.不幸的是,尽管付出了很多努力,但我还是无法在本地重现这个问题,因此我无法提供最小,完整和可验证的示例.此外,由于这是生产代码,我不得不在以下示例中更改表的名称.不过,我相信我会提出所有相关事实.
我们有四个表bucket_holder
,bucket
,item
并bucket_total
创建如下:
CREATE TABLE bucket_holder (
id SERIAL PRIMARY KEY,
bucket_holder_uid UUID NOT NULL
);
CREATE TABLE bucket (
id SERIAL PRIMARY KEY,
bucket_uid UUID NOT NULL,
bucket_holder_id INTEGER NOT NULL REFERENCES bucket_holder (id),
default_bucket BOOLEAN NOT NULL
);
CREATE TABLE item (
id SERIAL PRIMARY KEY,
item_uid UUID NOT NULL,
bucket_id INTEGER NOT NULL REFERENCES bucket (id),
amount NUMERIC NOT NULL
);
CREATE TABLE bucket_total (
bucket_id INTEGER NOT NULL REFERENCES bucket (id),
amount NUMERIC NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
适当的列上还有索引如下:
CREATE UNIQUE INDEX idx1 ON bucket_holder (bucket_holder_uid);
CREATE UNIQUE INDEX idx2 ON bucket (bucket_uid);
CREATE UNIQUE INDEX idx3 ON item (item_uid);
CREATE UNIQUE INDEX idx4 ON bucket_total (bucket_id);
Run Code Online (Sandbox Code Playgroud)
这个想法是bucket_holder
持有bucket
s,其中一个是a default_bucket
,bucket
s hold item
,每个bucket
都有一个bucket_total
包含所有item
s 的总和的唯一记录.
我们尝试item
按如下方式对表进行批量插入:
WITH
unnested AS (
SELECT *
FROM UNNEST(
ARRAY['00000000-0000-0000-0000-00000000001a', '00000000-0000-0000-0000-00000000002a']::UUID[],
ARRAY['00000000-0000-0000-0000-00000000001c', '00000000-0000-0000-0000-00000000002c']::UUID[],
ARRAY[1.11, 2.22]::NUMERIC[]
)
AS T(bucket_holder_uid, item_uid, amount)
),
inserted_item AS (
INSERT INTO item (bucket_id, item_uid, amount)
SELECT bucket.id, unnested.item_uid, unnested.amount
FROM unnested
JOIN bucket_holder ON unnested.bucket_holder_uid = bucket_holder.bucket_holder_uid
JOIN bucket ON bucket.bucket_holder_id = bucket_holder.id
JOIN bucket_total ON bucket_total.bucket_id = bucket.id
WHERE bucket.default_bucket
FOR UPDATE OF bucket_total
ON CONFLICT DO NOTHING
RETURNING bucket_id, amount
),
total_for_bucket AS (
SELECT bucket_id, SUM(amount) AS total
FROM inserted_item
GROUP BY bucket_id
)
UPDATE bucket_total
SET amount = amount + total_for_bucket.total
FROM total_for_bucket
WHERE bucket_total.bucket_id = total_for_bucket.bucket_id
Run Code Online (Sandbox Code Playgroud)
实际上,传入的数组是动态的,长度可达1000,但所有3个数组的长度都相同.始终对数组进行排序bucket_holder_uids
,以便确保不会发生死锁.关键ON CONFLICT DO NOTHING
在于我们应该能够处理某些item
已经存在的情况(冲突已经开始item_uid
).在这种情况下bucket_total
,当然不应该更新.
此查询假定适当bucket_holder
,bucket
并且bucket_total
记录已存在.查询失败是可以的,否则在实践中不会发生这种情况.以下是设置一些示例数据的示例:
INSERT INTO bucket_holder (bucket_holder_uid) VALUES ('00000000-0000-0000-0000-00000000001a');
INSERT INTO bucket (bucket_uid, bucket_holder_id, default_bucket) VALUES ('00000000-0000-0000-0000-00000000001b', (SELECT id FROM bucket_holder WHERE bucket_holder_uid = '00000000-0000-0000-0000-00000000001a'), TRUE);
INSERT INTO bucket_total (bucket_id, amount) VALUES ((SELECT id FROM bucket WHERE bucket_uid = '00000000-0000-0000-0000-00000000001b'), 0);
INSERT INTO bucket_holder (bucket_holder_uid) VALUES ('00000000-0000-0000-0000-00000000002a');
INSERT INTO bucket (bucket_uid, bucket_holder_id, default_bucket) VALUES ('00000000-0000-0000-0000-00000000002b', (SELECT id FROM bucket_holder WHERE bucket_holder_uid = '00000000-0000-0000-0000-00000000002a'), TRUE);
INSERT INTO bucket_total (bucket_id, amount) VALUES ((SELECT id FROM bucket WHERE bucket_uid = '00000000-0000-0000-0000-00000000002b'), 0);
Run Code Online (Sandbox Code Playgroud)
这个查询看起来已经为成千上万的item
s 做了正确的事情,但是对于少数几个item
s来说,bucket_total
已经更新了两倍的数量item
.我不知道它是否已被更新两次,或者它是否被更新了两次item
.但是在这些情况下,只item
插入了一个(因为存在唯一性约束,所以无论如何都不可能插入两次item_uid
).我们的日志表明,对于受影响的bucket
s,两个线程同时执行查询.
任何人都可以看到并解释此查询的任何问题,并说明如何重写它?
我们使用的是版本PG9.6.6
UPDATE
我们已经与一位核心postgres开发人员讨论了这个问题,他们显然没有看到并发问题.我们现在正在研究真正讨厌的可能性,例如索引损坏,或者pg bug的(远程)机会.
等待更多数据时的一些想法
根据您遇到的问题,听起来插入的项目 CTE 返回重复项或更新语句以某种方式执行了两次。两者听起来都很奇怪,可能是 pg bug?也许尝试尽可能简化查询
一些想法:看起来您首先将项目放入某个默认存储桶中。在这种情况下(一对多连接),连接到存储桶表没有多大意义。为什么不在holder表中只拥有默认的bucket id(或者为此拥有单独的cte)
该行似乎没有做任何事情: JOIN Bucket_total ON Bucket_total.bucket_id = Bucket.id
也许只需将数据插入项目表就足够了。为什么不将bucket_total作为视图(例如从项目中选择bucket_id,sum(金额)...)如果需要一段时间才能填充,则可以将其作为物化视图或报告表。或者,如果您在一天中多次运行该脚本,可能会在项目表上创建一个触发器,以在插入/删除时向存储桶添加/减去 1
假设您可以将查询减少为如下所示:
WITH
unnested AS (....),
INSERT INTO item (bucket_id, item_uid, amount)
SELECT bucket_holder2.dflt_bucket_id, unnested.item_uid, unnested.amount
FROM unnested
JOIN bucket_holder2 ON unnested.bucket_holder_uid = bucket_holder2.bucket_holder_uid
ON CONFLICT DO NOTHING
Run Code Online (Sandbox Code Playgroud)
更新 尝试在 9.6 上运行这些查询,效果很好。所以我认为查询和 pg 没有问题,可能是时候重新创建表/数据库了。另一个测试想法 - 您可以尝试将 Bucket_total 更新的“UPDATE”更改为“INSERT”,删除当前的唯一键并创建增量主键。这样你就可以捕获/修复双重插入(如果是这种情况)
归档时间: |
|
查看次数: |
390 次 |
最近记录: |