sql >> Databáze >  >> RDS >> PostgreSQL

Je SELECT nebo INSERT ve funkci náchylný k závodům?

Je to opakující se problém SELECT nebo INSERT při možném zatížení souběžného zápisu související s (ale odlišným od) UPSERT (což je INSERT nebo UPDATE ).

Tato funkce PL/pgSQL používá UPSERT (INSERT ... ON CONFLICT .. DO UPDATE ) na INSERT nebo SELECT jeden řádek :

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$;

Stále je tu malé okénko pro závodní podmínky. Aby naprostá jistota získáme 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<>zde hrajte

Toto pokračuje ve smyčce, dokud buď INSERT nebo SELECT uspěje. Volejte:

SELECT f_tag_id('possibly_new_tag');

Pokud následující příkazy ve stejné transakci spoléhat na existenci řádku a je skutečně možné, že jej jiné transakce aktualizují nebo smažou souběžně, existující řádek můžete uzamknout v SELECT příkaz s FOR SHARE .
Pokud se místo toho vloží řádek, je uzamčen (nebo není viditelný pro ostatní transakce) až do konce transakce.

Začněte běžným případem (INSERT vs SELECT ), aby to bylo rychlejší.

Související:

  • Získejte ID z podmíněného INSERT
  • Jak zahrnout vyloučené řádky do RETURNING z INSERT ... ON CONFLICT

Související (čisté SQL) řešení s INSERT nebo SELECT více řádků (sadu) najednou:

  • Jak používat RETURNING s ON CONFLICT v PostgreSQL?

Co je na tom tomu špatného? čisté řešení 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$;

Není to úplně špatné, ale nedokáže to zalepit mezeru, jak to řešil @FunctorSalad. Funkce může přijít s prázdným výsledkem, pokud se souběžná transakce pokusí udělat totéž ve stejnou dobu. Manuál:

Všechny příkazy jsou provedeny se stejným snímkem

Pokud souběžná transakce vloží stejnou novou značku o chvíli dříve, ale dosud nebyla potvrzena:

  • Část UPSERT je po čekání na dokončení souběžné transakce prázdná. (Pokud by se souběžná transakce měla vrátit zpět, stále vloží novou značku a vrátí nové ID.)

  • Část SELECT se také zobrazí prázdná, protože je založena na stejném snímku, kde není viditelná nová značka z (dosud nepotvrzené) souběžné transakce.

Nedostáváme nic . Ne tak, jak bylo zamýšleno. To je kontraintuitivní až naivní logika (a tam jsem se nechal chytit), ale tak funguje MVCC model Postgres – musí fungovat.

Toto nepoužívejte, pokud se více transakcí může pokusit vložit stejnou značku současně. Nebo smyčku, dokud skutečně nedostanete řadu. Smyčka se stejně sotva kdy spustí při běžném pracovním zatížení.

Postgres 9.4 nebo starší

Vzhledem k této (trochu zjednodušené) tabulce:

CREATE table tag (
  tag_id serial PRIMARY KEY
, tag    text   UNIQUE
);

téměř 100% bezpečné funkce pro vložení nového tagu / výběr stávajícího, by mohla vypadat takto.

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<>zde hrajte
Starý sqlfiddle

Proč ne 100%? Zvažte poznámky v příručce pro související UPSERT příklad:

  • https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE

Vysvětlení

  • Zkuste SELECT první . Vyhnete se tak výrazně dražším zpracování výjimek v 99,99 % případů.

  • Použijte CTE k minimalizaci (již tak malého) časového úseku pro podmínku závodu.

  • Časové okno mezi SELECT a INSERT v rámci jednoho dotazu je super malinká. Pokud nemáte velké souběžné zatížení nebo pokud můžete žít s výjimkou jednou za rok, můžete jednoduše ignorovat případ a použít příkaz SQL, který je rychlejší.

  • Není potřeba FETCH FIRST ROW ONLY (=LIMIT 1 ). Název značky je zjevně UNIQUE .

  • Odebrat FOR SHARE v mém příkladu, pokud obvykle nemáte souběžné DELETE nebo UPDATE na tabulce tag . Stojí to trochu výkonu.

  • Nikdy neuvádějte název jazyka:'plpgsql' . plpgsql je identifikátor . Citace může způsobit problémy a je tolerována pouze z důvodu zpětné kompatibility.

  • Nepoužívejte nepopisné názvy sloupců jako id nebo name . Když spojujete několik stolů (což děláte v relační DB) skončíte s více stejnými názvy a musíte použít aliasy.

Zabudováno do vaší funkce

Pomocí této funkce můžete značně zjednodušit FOREACH LOOP komu:

...
FOREACH TagName IN ARRAY $3
LOOP
   INSERT INTO taggings (PostId, TagId)
   VALUES   (InsertedPostId, f_tag_id(TagName));
END LOOP;
...

Rychlejší však jako jediný příkaz SQL s unnest() :

INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM   unnest($3) tag;

Nahrazuje celou smyčku.

Alternativní řešení

Tato varianta staví na chování UNION ALL s LIMIT klauzule:jakmile je nalezen dostatek řádků, zbytek se nikdy neprovede:

  • Jak vyzkoušet více možností SELECT, dokud nebude k dispozici výsledek?

Na základě toho můžeme outsourcovat INSERT do samostatné funkce. Pouze tam potřebujeme zpracování výjimek. Stejně bezpečné jako první řešení.

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$;

Která se používá v hlavní funkci:

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$;
  • To je o něco levnější, pokud většina hovorů vyžaduje pouze SELECT , protože dražší blok s INSERT obsahující EXCEPTION doložka se zadává jen zřídka. Dotaz je také jednodušší.

  • FOR SHARE zde není možné (není povoleno v UNION dotaz).

  • LIMIT 1 by nebylo nutné (testováno na str. 9.4). Postgres odvozuje LIMIT 1 z INTO _tag_id a provádí se pouze do doby, než je nalezen první řádek.



  1. Jak můžete v SQL seskupit podle rozsahů?

  2. Jaký je nejúčinnější způsob, jak zkontrolovat, zda záznam v Oracle existuje?

  3. Co je T-SQL?

  4. Seskupit SQLite podle/počítat hodiny, dny, týdny, rok