用于更新冲突的INSERT SELECT的语义没有返回

cpp*_*ner 14 sql postgresql concurrency common-table-expression

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

我们有四个表bucket_holder,bucket,itembucket_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持有buckets,其中一个是a default_bucket,buckets hold item,每个bucket都有一个bucket_total包含所有items 的总和的唯一记录.

我们尝试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)

这个查询看起来已经为成千上万的items 做了正确的事情,但是对于少数几个items来说,bucket_total已经更新了两倍的数量item.我不知道它是否已被更新两次,或者它是否被更新了两次item.但是在这些情况下,只item插入了一个(因为存在唯一性约束,所以无论如何都不可能插入两次item_uid).我们的日志表明,对于受影响的buckets,两个线程同时执行查询.

任何人都可以看到并解释此查询的任何问题,并说明如何重写它?

我们使用的是版本PG9.6.6

UPDATE

我们已经与一位核心postgres开发人员讨论了这个问题,他们显然没有看到并发问题.我们现在正在研究真正讨厌的可能性,例如索引损坏,或者pg bug的(远程)机会.

Mik*_*Twc 1

等待更多数据时的一些想法

根据您遇到的问题,听起来插入的项目 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”,删除当前的唯一键并创建增量主键。这样你就可以捕获/修复双重插入(如果是这种情况)