我写了一个函数来为一个简单的博客引擎创建帖子:
CREATE FUNCTION CreatePost(VARCHAR, TEXT, VARCHAR[])
RETURNS INTEGER AS $$
DECLARE
InsertedPostId INTEGER;
TagName VARCHAR;
BEGIN
INSERT INTO Posts (Title, Body)
VALUES ($1, $2)
RETURNING Id INTO InsertedPostId;
FOREACH TagName IN ARRAY $3 LOOP
DECLARE
InsertedTagId INTEGER;
BEGIN
-- I am concerned about this part.
BEGIN
INSERT INTO Tags (Name)
VALUES (TagName)
RETURNING Id INTO InsertedTagId;
EXCEPTION WHEN UNIQUE_VIOLATION THEN
SELECT INTO InsertedTagId Id
FROM Tags
WHERE Name = TagName
FETCH FIRST ROW ONLY;
END;
INSERT INTO Taggings (PostId, TagId)
VALUES (InsertedPostId, InsertedTagId);
END;
END LOOP;
RETURN InsertedPostId;
END;
$$ LANGUAGE 'plpgsql';
当多个用户同时删除标签并创建帖子时,是否容易出现竞争状况?
具体地说,交易(以及功能)是否可以防止此类竞争情况的发生?
我正在使用PostgreSQL 9.2.3。
最佳答案
这是在可能的并发写入负载下 SELECT
或INSERT
的反复出现的问题,与UPSERT
(INSERT
或UPDATE
)相关(但不同)。
对于Postgres 9.5或更高版本
使用新的UPSERT implementation INSERT ... ON CONFLICT .. DO UPDATE
,我们可以大大简化。 PL / pgSQL函数将单个行(标记)添加为INSERT
或SELECT
:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) 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$ LANGUAGE plpgsql;
比赛条件仍然很小。要使绝对确定,您需要获得一个ID: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
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$ LANGUAGE plpgsql;
这会一直循环直到INSERT
或SELECT
成功。call :
SELECT f_tag_id('possibly_new_tag');
如果同一事务中的后续命令依赖于该行的存在,并且实际上其他事务可能同时更新或删除该行,则可以使用 SELECT
锁定FOR SHARE
语句中的现有行。如果改为插入该行,则该行将一直锁定到事务结束为止。
如果大多数时间都插入了新行,请从
INSERT
开始以使其更快。有关:
一次针对
INSERT
或SELECT
多行(一组)的相关(纯SQL)解决方案:这种纯SQL解决方案有什么问题?
我以前也建议过此SQL函数:
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;
这不是完全错误,但是它无法填补漏洞,例如@FunctorSalad worked out in his added answer。如果并发事务尝试同时执行相同的操作,则该函数可能会得出空结果。具有CTE的查询中的所有语句实际上都是在同一时间执行的。 The manual:All the statements are executed with the same snapshot
如果并发事务稍早插入了相同的新标记,但尚未提交:
我们什么也没得到。不符合预期。这与朴素的逻辑是不合常理的(我被困在那里),但这就是Postgres的MVCC模型的工作原理-必须起作用。
因此,如果多个事务可以尝试同时插入同一标签,请不要使用此选项。 或循环,直到您实际获得一行为止。在常见的工作负载中几乎不会触发该循环。
原始答案(Postgres 9.4或更早版本)
给定此表(略有简化):
CREATE table tag (
tag_id serial PRIMARY KEY
, tag text UNIQUE
);
...实际上是100%安全的函数,用于插入新标签/选择现有标签,看起来像这样。为什么不100%?考虑notes in the manual for the related
UPSERT
example: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;
SQL Fiddle.说明
SELECT
。这样,您可以避免99.99%的时间处理异常昂贵的异常。SELECT
和INSERT
之间的时间窗口非常小。如果您没有繁重的并发负载,或者您可以每年遇到一次异常,则可以忽略这种情况,而使用SQL语句,这会更快。FETCH FIRST ROW ONLY
(= LIMIT 1
)。标签名称显然是UNIQUE
。FOR SHARE
上通常没有并发的DELETE
或UPDATE
,则在我的示例中删除 tag
。花费一点点性能。plpgsql
是一个标识符。 Quoting may cause problems,仅允许向后兼容。id
或name
之类的非描述性列名。当联接几个表时(这是在关系数据库中执行的操作),您最终会使用多个相同的名称,并且必须使用别名。内置在您的功能中
使用此功能,您可以在很大程度上将
FOREACH LOOP
简化为:...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
不过,使用 unnest()
作为单个SQL语句更快:INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
替换整个循环。替代解决方案
此变体基于带有
UNION ALL
子句的LIMIT
的行为:一旦找到足够的行,就永远不会执行其余的行:在此基础上,我们可以将
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$
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
,这会便宜一些,因为很少输入带有INSERT
子句的EXCEPTION
的更昂贵的块。查询也更简单。FOR SHARE
(在UNION
查询中不允许使用)。LIMIT 1
是不必要的(在9.4页中进行了测试)。 Postgres从LIMIT 1
派生INTO _tag_id
,并且仅执行到找到第一行为止。关于sql - SELECT或INSERT函数是否容易出现竞争状况?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15939902/