我有一个字符串向量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
错误是安全的......
答案 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
它还为可能的竞争条件留下了多更小的时间范围 事实上,时间框架应该是如此之小,以至于只有在大量并发负载或大型阵列下才会出现独特的违规行为。
除非您的问题是内置。你在输入数组本身有重复吗?在这种情况下,事务隔离不会对您有所帮助。敌人在里面!
考虑这个例子/解决方案:
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;
我让函数返回插入行的计数,这是完全可选的。
答案 2 :(得分:0)
我喜欢Erwin和Denis的答案,但另一种方法可能是让并发会话执行取消并加载到单独的临时表中,并可选择消除它们对目标表的重复,并选择单个会话从此临时表中,以适当的方式解析临时表内部重复项,插入目标表再次检查现有值,并删除所选临时表记录(使用公用表表达式在同一查询中)。
这将更加面向批处理,采用数据仓库提取 - 加载 - 转换范例的方式,但可以保证不需要处理任何唯一的约束问题。
其他优点/缺点适用,例如将最终插入与数据收集分离(可能的优势),以及需要经常对临时表进行抽真空(可能的缺点),这可能与Jon的情况无关,但可能有用在相同情况下向其他人提供信息。