To powtarzający się problem SELECT
lub INSERT
pod możliwym równoczesnym obciążeniem zapisu, związanym z (ale innym od) UPSERT
(czyli INSERT
lub UPDATE
).
Ta funkcja PL/pgSQL używa UPSERT (INSERT ... ON CONFLICT .. DO UPDATE
) do INSERT
lub SELECT
pojedynczy wiersz :
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$;
Wciąż jest małe okno na warunki wyścigu. Aby mieć absolutną pewność otrzymujemy identyfikator:
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<>graj tutaj
To utrzymuje pętlę aż do INSERT
lub SELECT
się powiedzie. Zadzwoń:
SELECT f_tag_id('possibly_new_tag');
Jeśli kolejne polecenia w tej samej transakcji polegać na istnieniu wiersza i faktycznie jest możliwe, że inne transakcje aktualizują lub usuwają go jednocześnie, możesz zablokować istniejący wiersz w SELECT
oświadczenie z FOR SHARE
.
Jeśli zamiast tego wiersz zostanie wstawiony, i tak jest zablokowany (lub niewidoczny dla innych transakcji) do końca transakcji.
Zacznij od typowego przypadku (INSERT
vs SELECT
), aby przyspieszyć.
Powiązane:
- Pobierz identyfikator z warunkowego INSERT
- Jak uwzględnić wykluczone wiersze w ZWROCIE z WSTAW ... W KONFLIKTACH
Powiązane (czysty SQL) rozwiązanie do INSERT
lub SELECT
wiele wierszy (zestaw) na raz:
- Jak używać RETURNING z ON CONFLICT w PostgreSQL?
Co jest to nie tak czyste rozwiązanie 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$;
Nie do końca źle, ale nie zamyka luki, jak opracowali @FunctorSalad. Funkcja może zwrócić pusty wynik, jeśli równoczesna transakcja próbuje zrobić to samo w tym samym czasie. Instrukcja:
Wszystkie instrukcje są wykonywane z tym samym zrzutem obrazu
Jeśli współbieżna transakcja wstawia ten sam nowy tag chwilę wcześniej, ale jeszcze nie została zatwierdzona:
-
Część UPSERT pojawia się pusta po odczekaniu na zakończenie równoczesnej transakcji. (Jeśli równoczesna transakcja powinna zostać wycofana, nadal wstawia nowy znacznik i zwraca nowy identyfikator).
-
Część SELECT również jest pusta, ponieważ opiera się na tej samej migawce, w której nowy tag z (jeszcze niezatwierdzonej) jednoczesnej transakcji nie jest widoczny.
Nie dostajemy nic . Nie tak, jak zamierzano. Jest to sprzeczne z intuicją w stosunku do naiwnej logiki (i mnie tam przyłapano), ale tak właśnie działa model Postgresa MVCC - musi działać.
Więc nie używaj tego, jeśli wiele transakcji może próbować wstawić ten sam tag w tym samym czasie. Lub pętla, aż faktycznie otrzymasz wiersz. Pętla i tak prawie nigdy nie zostanie wyzwolona w typowych obciążeniach roboczych.
Postgres 9.4 lub starszy
Biorąc pod uwagę tę (nieco uproszczoną) tabelę:
CREATE table tag (
tag_id serial PRIMARY KEY
, tag text UNIQUE
);
Prawie 100% bezpieczny funkcja do wstawienia nowego tagu / wyboru istniejącego, może wyglądać tak.
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<>graj tutaj
Stary sqlfiddle
Dlaczego nie w 100%? Rozważ uwagi w instrukcji dla powiązanego UPSERT
przykład:
- https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
Wyjaśnienie
-
Wypróbuj
SELECT
pierwszy . W ten sposób unikniesz znacznie droższego obsługa wyjątków przez 99,99% przypadków. -
Użyj CTE, aby zminimalizować (już mały) przedział czasowy dla warunków wyścigu.
-
Okno czasowe między
SELECT
iINSERT
w ramach jednego zapytania jest bardzo mały. Jeśli nie masz dużego współbieżnego obciążenia lub możesz żyć z wyjątkiem raz w roku, możesz po prostu zignorować przypadek i użyć instrukcji SQL, która jest szybsza. -
Nie ma potrzeby
FETCH FIRST ROW ONLY
(=LIMIT 1
). Nazwa tagu to oczywiścieUNIQUE
. -
Usuń
FOR SHARE
w moim przykładzie, jeśli zwykle nie masz jednoczesnegoDELETE
lubUPDATE
w tabelitag
. Kosztuje trochę wydajności. -
Nigdy nie cytuj nazwy języka:
'plpgsql'.plpgsql
jest identyfikatorem . Cytowanie może powodować problemy i jest tolerowane tylko ze względu na kompatybilność wsteczną. -
Nie używaj nieopisowych nazw kolumn, takich jak
id
lubname
. Kiedy dołączasz do kilku stołów (co robisz w relacyjnej bazie danych) otrzymujesz wiele identycznych nazw i musisz używać aliasów.
Wbudowany w twoją funkcję
Korzystając z tej funkcji, możesz znacznie uprościć swoją FOREACH LOOP
do:
...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
Szybciej jednak, jak pojedyncza instrukcja SQL z unnest()
:
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
Zastępuje całą pętlę.
Alternatywne rozwiązanie
Ten wariant opiera się na zachowaniu UNION ALL
z LIMIT
klauzula:jak tylko zostanie znaleziona wystarczająca liczba wierszy, reszta nigdy nie zostanie wykonana:
- Jak wypróbować wiele opcji SELECT, dopóki wynik nie będzie dostępny?
Bazując na tym, możemy zlecić na zewnątrz INSERT
w oddzielną funkcję. Tylko tam potrzebujemy obsługi wyjątków. Tak samo bezpieczne jak pierwsze rozwiązanie.
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$;
Który jest używany w głównej funkcji:
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$;
-
Jest to nieco tańsze, jeśli większość połączeń wymaga tylko
SELECT
, ponieważ droższy blok zINSERT
zawierająceEXCEPTION
klauzula jest rzadko wprowadzana. Zapytanie jest również prostsze. -
FOR SHARE
nie jest tutaj możliwe (niedozwolone wUNION
zapytanie). -
LIMIT 1
nie byłoby konieczne (testowane na stronie 9.4). Postgres wywodziLIMIT 1
zINTO _tag_id
i wykonuje się tylko do momentu znalezienia pierwszego wiersza.