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
    更昂贵的块。查询也更简单

  • 此处不允许共享的
    (在
    联合查询中不允许)

  • 不需要限制1
    (在第9.4页中测试)。Postgres从
    导出
    LIMIT 1
    到_tag_id
    ,并且只在找到第一行之前执行


即使在使用Postgres 9.5中引入的关于冲突的
条款时,仍有一些需要注意的地方。使用与@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