1

我有一个字符串向量data,其中包含要插入到名为foos. 表中可能已经存在一些元素data,所以我必须注意这些。

我使用的解决方案首先将data向量转换为虚拟表old_and_new;然后它构建虚拟表old,其中包含已经存在的元素foosnew然后,它用真正新的元素构造虚拟表。最后,它在 table 中插入新元素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错误是安全的……

4

3 回答 3

3

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

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

您不能像执行 a 那样锁定插入select for update,因为这些行还不存在。

可以锁定整个表,但这比序列化事务更麻烦。

您可以使用咨询锁,但要特别注意死锁。对新密钥进行排序,以便以一致的、可预测的顺序获得锁。(希望对 PG 的源代码有更多了解的人会加入,但我猜在可序列化隔离级别中使用的谓词锁正好可以做到这一点。)

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

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

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

而且我确定我忘记了一些其他选项...

于 2013-05-03T14:01:58.180 回答
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

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

数组中的骗子?

除非,如果您的问题是内置的. 输入数组本身是否有重复项?在这种情况下,事务隔离对您没有帮助。敌人在里面!

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

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在子查询中使用以消除“休眠代理”,即重复项。

人们往往会忘记,骗子可能会出现导入数据中。

全自动化

此函数是一种永久处理并发的方法。如果 aUNIQUE_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 演示

于 2013-05-03T17:28:02.750 回答
0

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

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

其他优点/缺点也适用,例如将最终插入与数据收集分离(可能的优点),以及需要经常清理临时表(可能的缺点),这可能与 Jon 的情况无关,但可能对其他人有用的信息在同样的情况下。

于 2013-05-04T07:35:34.497 回答