SELECT
这是可能的并发写入负载或INSERT
在可能的并发写入负载下反复出现的问题,与(但不同于)UPSERT
(即INSERT
或UPDATE
)有关。
此 PL/pgSQL 函数使用UPSERTINSERT ... ON CONFLICT .. DO UPDATE
INSERT
( )或单行:SELECT
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
SELECT tag_id -- only if row existed before
FROM tag
WHERE tag = _tag
INTO _tag_id;
IF NOT FOUND THEN
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
INTO _tag_id;
END IF;
END
$func$;
竞争条件仍然有一个很小的窗口。为了确保我们得到一个 ID:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
SELECT tag_id
FROM tag
WHERE tag = _tag
INTO _tag_id;
EXIT WHEN FOUND;
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
INTO _tag_id;
EXIT WHEN FOUND;
END LOOP;
END
$func$;
db<>在这里摆弄
这会一直循环,直到其中一个INSERT
或SELECT
成功。称呼:
SELECT f_tag_id('possibly_new_tag');
如果同一事务中的后续命令依赖于该行的存在,并且实际上有可能其他事务同时更新或删除它,您可以使用 锁定SELECT
语句中的现有行FOR SHARE
。
如果该行被插入,它将被锁定(或对其他事务不可见),直到事务结束。
从常见情况(INSERT
vs SELECT
)开始,以使其更快。
有关的:
INSERT
一次或多SELECT
行(一组)的相关(纯SQL)解决方案:
这个纯 SQL 解决方案有什么问题?
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE sql 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$;
并非完全错误,但它未能像@FunctorSalad 解决的那样堵住漏洞。如果并发事务尝试同时执行相同操作,该函数可能会得出一个空结果。手册:
所有语句都使用相同的快照执行
如果并发事务稍早插入了相同的新标签,但尚未提交:
我们一无所获。不像预期的那样。这对幼稚的逻辑是违反直觉的(我被抓住了),但这就是 Postgres 的 MVCC 模型的工作原理——必须工作。
因此,如果多个事务可以尝试同时插入同一个标签,请不要使用此选项。或者循环直到你真正得到一行。无论如何,循环几乎不会在常见的工作负载中触发。
Postgres 9.4 或更高版本
鉴于此(略微简化)表:
CREATE table tag (
tag_id serial PRIMARY KEY
, tag text UNIQUE
);
插入新标签/选择现有标签的几乎 100% 安全功能可能如下所示。
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int)
LANGUAGE plpgsql 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$;
db<>fiddle here
旧sqlfiddle
为什么不是100%?UPSERT
考虑相关示例的手册中的注释:
解释
尝试第SELECT
一个。通过这种方式,您可以在 99.99% 的时间内避免成本更高的异常处理。
使用CTE最小化竞争条件的(已经很小的)时间段。
SELECT
一次查询和INSERT
一次查询之间的时间窗口非常小。如果您没有繁重的并发负载,或者您可以每年忍受一次异常,您可以忽略这种情况并使用 SQL 语句,这样会更快。
不需要FETCH FIRST ROW ONLY
(= LIMIT 1
)。标签名称显然是UNIQUE
.
FOR SHARE
如果您通常没有 concurrentDELETE
或UPDATE
on the table ,请在我的示例中删除tag
。花费一点点性能。
永远不要引用语言名称:'plpgsql'。plpgsql
是一个标识符。引用可能会导致问题,并且只允许向后兼容。
不要使用非描述性的列名,如id
or name
。当连接几个表时(这是您在关系数据库中所做的),您最终会得到多个相同的名称并且必须使用别名。
内置到您的函数中
使用此功能,您可以在很大程度上简化您FOREACH LOOP
:
...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
但是,作为单个 SQL 语句,速度更快unnest()
:
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
替换整个循环。
替代解决方案
这个变体建立在UNION ALL
withLIMIT
子句的行为之上:只要找到足够多的行,其余的就永远不会执行:
在此基础上,我们可以将其外包INSERT
到一个单独的功能中。只有在那里我们需要异常处理。与第一个解决方案一样安全。
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
RETURNS int
LANGUAGE plpgsql 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$;
在主要功能中使用:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql 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$;
如果大多数调用只需要 ,这会便宜一些,因为很少输入包含该子句SELECT
的更昂贵的块。查询也更简单。INSERT
EXCEPTION
FOR SHARE
在这里是不可能的(不允许在UNION
查询中)。
LIMIT 1
没有必要(在第 9.4 页测试)。Postgres 派生LIMIT 1
自INTO _tag_id
并且仅在找到第一行之前执行。