我们的生产系统遇到了一个非常特殊的问题。不幸的是,尽管付出了很多努力,但我仍无法在本地重现该问题,因此我无法提供一个简单,完整和可验证的示例。另外,由于这是生产代码,因此在以下示例中,我不得不更改表的名称。但是我相信我会介绍所有相关事实。
我们有四个表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
);
在适当的列上也有索引,如下所示:
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);
这个想法是,一个bucket_holder
持有bucket
,其中一个是default_bucket
,bucket
个持有item
s,每个{{1 }}具有唯一的bucket
记录,其中包含所有bucket_total
的金额之和。
我们正在尝试向item
表中进行批量插入,如下所示:
item
实际上,传入的数组是动态的,长度最大为1000,但是所有3个数组的长度相同。始终对数组进行排序,以便使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
处于顺序状态,以确保不会发生死锁。 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);
执行了正确的操作,但是对于少数item
s,item
的更新量是bucket_total
。我不知道它是否已被更新两次,或者是否被更新过一次item
的两倍。但是,在这些情况下,仅插入了一个item
(由于item
上存在唯一性约束,因此无论如何都不能插入两次)。我们的日志表明,对于受影响的item_uid
,两个线程正在同时执行查询。
任何人都可以看到并解释此查询的任何问题,并指出如何将其重写吗?
我们正在使用版本PG9.6.6
更新
我们已经与一位核心postgres开发人员进行了交谈,他显然在这里没有发现并发问题。我们现在正在研究诸如索引损坏或pg错误(远程)的可能性之类的令人讨厌的可能性。
答案 0 :(得分:1)
等待更多数据时的一些想法
根据您遇到的问题,听起来像是insert_items CTE 返回dups或以某种方式执行了两次更新语句。 听起来都怪异,可能是pg bug?也许尝试尽可能简化查询
一些想法: 好像您先将项目放入某个默认存储桶。它没有 在这种情况下,具有加入桶表的意义(一对多)。 为什么不只是在持有人表中有默认存储区ID(或为此有单独的CTE)
该行似乎没有任何作用: JOIN bucket_total ON bucket_total.bucket_id = bucket.id
仅将数据插入项目表中就足够了。 为什么不将bucket_total作为视图(例如从项中选择select bucket_id,sum(amount)...) 如果需要一些时间来填充,则可以将其作为实例化视图或报告表。 或者,如果您在一天中多次运行该脚本, 可能会在项目表上创建一个触发器,以在插入/删除操作中向存储桶添加/减去1
假设您可以将查询简化为以下内容:
sum
更新 试图在9.6上运行这些查询,但效果很好。因此,我认为查询和pg没有问题,可能是时候重新创建表/数据库了。 测试的另一种思路-您可以尝试将“ UPDATE”更改为“ INSERT”以进行bucket_total更新,删除当前唯一键并创建增量主键。这样,您就可以捕获/修复双重插入(如果是这种情况)