Sql LIMIT子句:一旦找到足够的行,其余的就永远不会执行:
在此基础上,我们可以将Sql LIMIT子句:一旦找到足够的行,其余的就永远不会执行:,sql,postgresql,concurrency,plpgsql,upsert,Sql,Postgresql,Concurrency,Plpgsql,Upsert,在此基础上,我们可以将INSERT外包到单独的功能中。只有在那里,我们才需要异常处理。就像第一个解决方案一样安全 CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int) RETURNS int AS $func$ BEGIN INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id; EXCEPTION WHEN UNIQUE_VIOL
INSERT
外包到单独的功能中。只有在那里,我们才需要异常处理。就像第一个解决方案一样安全
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
RETURNS int AS
$func$
BEGIN
INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$ LANGUAGE plpgsql;
在主功能中使用:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS
$func$
WITH ins AS (
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
)
SELECT tag_id FROM ins
UNION ALL
SELECT tag_id FROM tag WHERE tag = _tag
LIMIT 1
$func$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS
$func$
BEGIN
LOOP
SELECT tag_id FROM tag WHERE tag = _tag
UNION ALL
SELECT f_insert_tag(_tag) -- only executed if tag not found
LIMIT 1 -- not strictly necessary, just to be clear
INTO _tag_id;
EXIT WHEN _tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$ LANGUAGE plpgsql;
- 如果大多数调用只需要
,这会稍微便宜一些,因为很少输入包含SELECT
子句的EXCEPTION
更昂贵的块。查询也更简单INSERT
- 此处不允许共享的
(在
联合查询中不允许)
(在第9.4页中测试)。Postgres从不需要限制1
,并且只在找到第一行之前执行导出
到_tag_idLIMIT 1
条款时,仍有一些需要注意的地方。使用与@Erwin Brandstetter答案中相同的函数和示例表,如果我们这样做:
Session 1: begin;
Session 2: begin;
Session 1: select f_tag_id('a');
f_tag_id
----------
11
(1 row)
Session 2: select f_tag_id('a');
[Session 2 blocks]
Session 1: commit;
[Session 2 returns:]
f_tag_id
----------
NULL
(1 row)
因此f_tag_id
在会话2中返回了NULL
,这在单线程世界中是不可能的
如果我们将事务隔离级别提高到可重复读取
(或更强的可序列化
),会话2将抛出错误:由于并发更新
而无法序列化访问。因此,至少没有“不可能”的结果,但不幸的是,我们现在需要准备重试事务
编辑:使用可重复读取
或可序列化
,如果会话1插入标记a
,则会话2插入b
,然后会话1尝试插入b
,会话2尝试插入a
,一个会话检测到死锁:
ERROR: deadlock detected
DETAIL: Process 14377 waits for ShareLock on transaction 1795501; blocked by process 14363.
Process 14363 waits for ShareLock on transaction 1795503; blocked by process 14377.
HINT: See server log for query details.
CONTEXT: while inserting index tuple (0,3) in relation "tag"
SQL function "f_tag_id" statement 1
收到死锁错误的会话回滚后,另一个会话继续。所以我想我们应该像对待序列化失败一样对待死锁,然后在这种情况下重试
或者,以一致的顺序插入标记,但如果它们没有全部添加到一个位置,则这并不容易。选择进行更新
仅适用于更新,因为插入的行不存在,因此无需锁定。这是一个非常棒的记录,@Erwin。我使用的是Postgres 9.6,我的SELECT
或INSERT
解决方案基于您的第一个代码(以及锁定),只是我将其作为独立语句执行,而不是作为函数执行。然而,有时我从语句中得到的结果是空的。这是在一些其他事务已经插入了冲突行的情况下。但是,随后从表中选择将生成该行。我认为这不是预期的行为,或者是吗?@twoflower:这表明当您的事务尝试选择该行时,并发事务尚未提交。使用冲突(标记)上的DO UPDATE SET tag=t.tag可以避免该问题,其中FALSE类似于上面建议和解释的。你试过了吗?是的,我正在用。我将尝试准备一些这种行为的最小样本。@twoflower,ErwinBrandstetter:我遇到了与@twoflower类似的问题;我在下面添加了一个示例作为答案(很抱歉,我的答案是comment-y,但这不是很容易理解的格式,我没有开始新的问题,因为这似乎与原始问题非常相关)。在这种情况下,DO UPDATE SET tag=t.tag,其中FALSE
似乎没有什么区别。@Kudi:锁定整个表会有效地禁用并发性,并且任务变得微不足道。但相对来说,这是非常昂贵的,所以你通常想要避免它。(它还可能带来死锁的新问题。)非常有用的补充!(是的,我会将其视为序列化失败,并在SERIALIZABLE
事务隔离中重试。但在默认READ COMMITTED
中处理此问题而不出现异常要便宜得多。)
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int) AS
$func$
BEGIN
LOOP
BEGIN
WITH sel AS (SELECT t.tag_id FROM tag t WHERE t.tag = _tag FOR SHARE)
, ins AS (INSERT INTO tag(tag)
SELECT _tag
WHERE NOT EXISTS (SELECT 1 FROM sel) -- only if not found
RETURNING tag.tag_id) -- qualified so no conflict with param
SELECT sel.tag_id FROM sel
UNION ALL
SELECT ins.tag_id FROM ins
INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- insert in concurrent session?
RAISE NOTICE 'It actually happened!'; -- hardly ever happens
END;
EXIT WHEN tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$ LANGUAGE plpgsql;
...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
RETURNS int AS
$func$
BEGIN
INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS
$func$
BEGIN
LOOP
SELECT tag_id FROM tag WHERE tag = _tag
UNION ALL
SELECT f_insert_tag(_tag) -- only executed if tag not found
LIMIT 1 -- not strictly necessary, just to be clear
INTO _tag_id;
EXIT WHEN _tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$ LANGUAGE plpgsql;
Session 1: begin;
Session 2: begin;
Session 1: select f_tag_id('a');
f_tag_id
----------
11
(1 row)
Session 2: select f_tag_id('a');
[Session 2 blocks]
Session 1: commit;
[Session 2 returns:]
f_tag_id
----------
NULL
(1 row)
ERROR: deadlock detected
DETAIL: Process 14377 waits for ShareLock on transaction 1795501; blocked by process 14363.
Process 14363 waits for ShareLock on transaction 1795503; blocked by process 14377.
HINT: See server log for query details.
CONTEXT: while inserting index tuple (0,3) in relation "tag"
SQL function "f_tag_id" statement 1