INSERT SELECT冲突更新的语义不返回

时间:2018-11-03 08:40:36

标签: sql postgresql concurrency common-table-expression

我们的生产系统遇到了一个非常特殊的问题。不幸的是,尽管付出了很多努力,但我仍无法在本地重现该问题,因此我无法提供一个简单,完整和可验证的示例。另外,由于这是生产代码,因此在以下示例中,我不得不更改表的名称。但是我相信我会介绍所有相关事实。

我们有四个表bucket_holderbucketitembucket_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_bucketbucket个持有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_totalbucket_holderbucket记录。查询失败是可以的,否则在实践中不会发生这种情况。这是设置一些示例数据的示例:

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错误(远程)的可能性之类的令人讨厌的可能性。

1 个答案:

答案 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更新,删除当前唯一键并创建增量主键。这样,您就可以捕获/修复双重插入(如果是这种情况)