忽略并发插入中的错误

时间:2013-05-03 13:20:01

标签: sql postgresql concurrency duplicate-removal sql-insert

我有一个字符串向量data,其中包含我要插入名为foos的表中的项目。 data中的某些元素可能已存在于表中,因此我必须注意这些元素。

我正在使用的解决方案首先将data向量转换为虚拟表old_and_new;然后构建虚拟表old,其中包含foos中已存在的元素;然后,它用元素构造虚拟表new 这真的很新。最后,它在表foos中插入新元素。

WITH   old_and_new AS (SELECT unnest ($data :: text[]) AS foo),
       old AS (SELECT foo FROM foos INNER JOIN old_and_new USING (foo)),
       new AS (SELECT * FROM old_and_new EXCEPT SELECT * FROM old)
INSERT INTO foos (foo) SELECT foo FROM new

这在非并发设置中工作正常,但如果并发线程则失败 尝试同时插入相同的新元素。我知道我可以解决这个问题 通过将隔离级别设置为serializable,但这非常严厉。

还有其他方法可以解决这个问题吗?如果只有一种方法 告诉PostgreSQL忽略INSERT错误是安全的......

3 个答案:

答案 0 :(得分:3)

  

还有其他方法可以解决这个问题吗?

有很多,但没有一个是灵丹妙药......

您无法锁定插件,就像您可以执行select for update一样,因为行尚不存在。

可以锁定整个表格,但这更加严重,可以将你的交易序列化。

您可以使用咨询锁,但要对死锁保持警惕。对新密钥进行排序,以便以一致,可预测的顺序获取锁。 (对PG的源代码更有见识的人希望能够插入,但我猜测在可序列化隔离级别中使用的谓词锁相当于这样做。)

在纯sql中,您还可以使用do语句逐个遍历行,并在发生错误时捕获错误:

同样,您可以创建一个复杂的upsert函数,并为每个数据调用一次......

如果您在应用级别构建$ data,则可以逐个运行插入并忽略错误。

而且我确定我忘了一些额外的选择......

答案 1 :(得分:2)

无论你采取什么行动(@Denis给了你很多选择),这个重写的INSERT命令将更多更快

INSERT INTO foos (foo)
SELECT n.foo
FROM   unnest ($data::text[]) AS n(foo)
LEFT   JOIN foos o USING (foo)
WHERE  o.foo IS NULL

它还为可能的竞争条件留下了更小的时间范围 事实上,时间框架应该是如此之小,以至于只有在大量并发负载或大型阵列下才会出现独特的违规行为。

数组中的Dupes?

除非您的问题是内置。你在输入数组本身有重复吗?在这种情况下,事务隔离不会对您有所帮助。敌人在里面!

考虑这个例子/解决方案:

INSERT INTO foos (foo)
SELECT n.foo
FROM  (SELECT DISTINCT foo FROM unnest('{foo,bar,foo,baz}'::text[]) AS foo) n
LEFT   JOIN foos o USING (foo)
WHERE  o.foo IS NULL

我在子查询中使用DISTINCT来消除“睡眠代理”,a.k.a。重复。

人们往往会忘记这些欺骗行为可能来自 导入数据。

完全自动化

此函数是处理并发性的一种方法。如果发生UNIQUE_VIOLATION,则会重试INSERT。新呈现的行将自动从新尝试中排除。

处理相反的问题,可能同时删除了一行 - 这不会重新插入。有人可能会说,这个结果是可以的,因为这样的DELETE同时发生了。如果您想阻止这种情况,请使用SELECT ... FOR SHARE来保护并发DELETE行。

CREATE OR REPLACE FUNCTION f_insert_array(_data text[], OUT ins_ct int) AS
$func$
BEGIN

LOOP
   BEGIN

   INSERT INTO foos (foo)
   SELECT n.foo
   FROM  (SELECT DISTINCT foo FROM unnest(_data) AS foo) n
   LEFT   JOIN foos o USING (foo)
   WHERE  o.foo IS NULL;

   GET DIAGNOSTICS ins_ct = ROW_COUNT;
   RETURN;

   EXCEPTION WHEN UNIQUE_VIOLATION THEN     -- tag.tag has UNIQUE constraint.
      RAISE NOTICE 'It actually happened!'; -- hardly ever happens
   END;
END LOOP;

END
$func$
  LANGUAGE plpgsql;

我让函数返回插入行的计数,这是完全可选的。

-> SQLfiddle demo

答案 2 :(得分:0)

我喜欢Erwin和Denis的答案,但另一种方法可能是让并发会话执行取消并加载到单独的临时表中,并可选择消除它们对目标表的重复,并选择单个会话从此临时表中,以适当的方式解析临时表内部重复项,插入目标表再次检查现有值,并删除所选临时表记录(使用公用表表达式在同一查询中)。

这将更加面向批处理,采用数据仓库提取 - 加载 - 转换范例的方式,但可以保证不需要处理任何唯一的约束问题。

其他优点/缺点适用,例如将最终插入与数据收集分离(可能的优势),以及需要经常对临时表进行抽真空(可能的缺点),这可能与Jon的情况无关,但可能有用在相同情况下向其他人提供信息。