Това е повтарящият се проблем на SELECT
или INSERT
при възможно едновременно натоварване при запис, свързано с (но различно от) UPSERT
(което е INSERT
или UPDATE
).
Тази функция PL/pgSQL използва UPSERT (INSERT ... 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
срещу SELECT
), за да го направите по-бързо.
Свързано:
- Вземете идентификатор от условно INSERT
- Как да включите изключени редове в ВРЪЩАНЕ от INSERT... ON CONFLICT
Свързано (чисто SQL) решение за INSERT
или SELECT
много реда (комплект) наведнъж:
- Как да използвам RETURNING с ON CONFLICT в PostgreSQL?
Какво не е наред с това чисто 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. Функцията може да излезе с празен резултат, ако едновременна транзакция се опита да направи същото по едно и също време. Ръководството:
Всички оператори се изпълняват с една и съща моментна снимка
Ако едновременна транзакция вмъкне същия нов маркер момент по-рано, но все още не е ангажирана:
-
Частта UPSERT излиза празна, след като се изчаква едновременната транзакция да завърши. (Ако едновременната транзакция трябва да се върне назад, тя все още вмъква новия маркер и връща нов идентификатор.)
-
Частта SELECT също е празна, тъй като е базирана на същата моментна снимка, където новият маркер от (все още неангажирана) едновременна транзакция не се вижда.
Не получаваме нищо . Не както е предвидено. Това е противоположно на интуицията на наивната логика (и аз бях хванат там), но така работи моделът MVCC на Postgres – трябва да работи.
Така че не използвайте това, ако няколко транзакции могат да се опитат да вмъкнат един и същ маркер едновременно. Или цикъл, докато действително получите ред. Така или иначе цикълът едва ли ще бъде задействан при обичайни работни натоварвания.
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<>цигулка тук
Стар sqlfiddle
Защо не 100%? Обърнете внимание на бележките в ръководството за свързания UPSERT
пример:
- https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
Обяснение
-
Опитайте
SELECT
първо . По този начин избягвате значително по-скъпите обработка на изключения в 99,99% от времето. -
Използвайте CTE, за да минимизирате (вече малкия) времеви интервал за условията на състезанието.
-
Времевият прозорец между
SELECT
иINSERT
в рамките на една заявка е супер мъничко. Ако нямате голямо едновременно натоварване или ако можете да живеете с изключение веднъж годишно, можете просто да игнорирате случая и да използвате SQL израза, който е по-бърз. -
Няма нужда от
FETCH FIRST ROW ONLY
(=LIMIT 1
). Името на маркера очевидно еUNIQUE
. -
Премахнете
FOR SHARE
в моя пример, ако обикновено нямате едновременноDELETE
илиUPDATE
на таблицатаtag
. Струва малко производителност. -
Никога не цитирайте името на езика:
'plpgsql'.plpgsql
е идентификатор . Цитирането може да причини проблеми и се толерира само за обратна съвместимост. -
Не използвайте неописателни имена на колони като
id
илиname
. Когато се присъединявате към няколко маси (което правите в релационна DB) в крайна сметка получавате множество идентични имена и трябва да използвате псевдоними.
Вграден във вашата функция
Използвайки тази функция, бихте могли до голяма степен да опростите вашия 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
с LIMIT
клауза:веднага щом се намерят достатъчно редове, останалите никога не се изпълняват:
- Начин да опитате няколко SELECT, докато резултатът е наличен?
Въз основа на това можем да изнесем 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
и се изпълнява само докато не бъде намерен първият ред.