PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Czy SELECT lub INSERT w funkcji podatne na wyścigi?

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 i INSERT 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ście UNIQUE .

  • Usuń FOR SHARE w moim przykładzie, jeśli zwykle nie masz jednoczesnego DELETE lub UPDATE w tabeli tag . 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 lub name . 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 z INSERT zawierające EXCEPTION klauzula jest rzadko wprowadzana. Zapytanie jest również prostsze.

  • FOR SHARE nie jest tutaj możliwe (niedozwolone w UNION zapytanie).

  • LIMIT 1 nie byłoby konieczne (testowane na stronie 9.4). Postgres wywodzi LIMIT 1 z INTO _tag_id i wykonuje się tylko do momentu znalezienia pierwszego wiersza.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak działają widoki bariery bezpieczeństwa PostgreSQL?

  2. Jak działa Width_Bucket() w PostgreSQL

  3. Sortowanie alfanumeryczne w PostgreSQL

  4. Zautomatyzowane aktualizacje klastrów PostgreSQL w chmurze niemal zerowe przestoje (część II)

  5. Analiza porównawcza zarządzanych rozwiązań chmurowych PostgreSQL — część pierwsza:Amazon Aurora