sql >> Database teknologi >  >> RDS >> PostgreSQL

Er SELECT eller INSERT i en funktion tilbøjelig til raceforhold?

Det er det tilbagevendende problem med SELECT eller INSERT under mulig samtidig skrivebelastning, relateret til (men forskellig fra) UPSERT (som er INSERT eller UPDATE ).

Denne PL/pgSQL-funktion bruger UPSERT (INSERT ... ON CONFLICT .. DO UPDATE ) til INSERT eller SELECT en enkelt række :

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

Der er stadig et lille vindue til en løbstilstand. For at være helt sikker vi får et 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<>spil her

Dette fortsætter med at sløjfe indtil enten INSERT eller SELECT lykkes.Ring:

SELECT f_tag_id('possibly_new_tag');

Hvis efterfølgende kommandoer i samme transaktion stole på eksistensen af ​​rækken, og det er faktisk muligt, at andre transaktioner opdaterer eller sletter den samtidig, du kan låse en eksisterende række i SELECT erklæring med FOR SHARE .
Hvis rækken bliver indsat i stedet, er den låst (eller ikke synlig for andre transaktioner) indtil slutningen af ​​transaktionen alligevel.

Start med det almindelige tilfælde (INSERT vs SELECT ) for at gøre det hurtigere.

Relateret:

  • Få id fra en betinget INSERT
  • Sådan inkluderes ekskluderede rækker i RETURNING from INSERT ... ON CONFLICT

Relateret (ren SQL) løsning til INSERT eller SELECT flere rækker (et sæt) på én gang:

  • Hvordan bruger man RETURNING med ON CONFLICT i PostgreSQL?

Hvad er der galt med dette ren SQL-løsning?

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

Ikke helt forkert, men det lykkes ikke at tætne et smuthul, som @FunctorSalad udarbejdede. Funktionen kan komme med et tomt resultat, hvis en samtidig transaktion forsøger at gøre det samme på samme tid. Manualen:

Alle udsagn udføres med det samme øjebliksbillede

Hvis en samtidig transaktion indsætter det samme nye tag et øjeblik tidligere, men ikke har forpligtet sig endnu:

  • UPSERT-delen kommer op tom efter at have ventet på, at den samtidige transaktion er afsluttet. (Hvis den samtidige transaktion skulle rulle tilbage, indsætter den stadig det nye tag og returnerer et nyt ID.)

  • SELECT-delen vises også tom, fordi den er baseret på det samme øjebliksbillede, hvor det nye tag fra den (endnu ikke-forpligtede) samtidige transaktion ikke er synlig.

Vi får ingenting . Ikke efter hensigten. Det er kontraintuitivt i forhold til naiv logik (og jeg blev fanget der), men det er sådan, MVCC-modellen af ​​Postgres fungerer - skal fungere.

Så brug ikke dette, hvis flere transaktioner kan forsøge at indsætte det samme tag på samme tid. Eller sløjfe, indtil du rent faktisk får en række. Løkken vil næppe nogensinde blive udløst i almindelige arbejdsbelastninger alligevel.

Postgres 9.4 eller ældre

I betragtning af denne (lidt forenklede) tabel:

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

En næsten 100 % sikker funktion til at indsætte nyt tag / vælge eksisterende, kunne se sådan ud.

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<>spil her
Gamle sqlfiddle

Hvorfor ikke 100%? Overvej bemærkningerne i manualen til den relaterede UPSERT eksempel:

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

Forklaring

  • Prøv SELECT først . På denne måde undgår du det betydeligt dyrere undtagelseshåndtering 99,99 % af tiden.

  • Brug en CTE til at minimere det (allerede lille) tidsvindue for løbets tilstand.

  • Tidsvinduet mellem SELECT og INSERT inden for én forespørgsel er super lille. Hvis du ikke har tung samtidig belastning, eller hvis du kan leve med en undtagelse en gang om året, kan du bare ignorere sagen og bruge SQL-sætningen, som er hurtigere.

  • Intet behov for FETCH FIRST ROW ONLY (=LIMIT 1 ). Tagnavnet er tydeligvis UNIQUE .

  • Fjern FOR SHARE i mit eksempel, hvis du normalt ikke har samtidig DELETE eller UPDATE på tabellen tag . Det koster en lille smule ydeevne.

  • Citér aldrig sprognavnet:'plpgsql' . plpgsql er en identifikator . Citering kan forårsage problemer og tolereres kun for bagudkompatibilitet.

  • Brug ikke ikke-beskrivende kolonnenavne som id eller name . Når du tilmelder dig et par borde (det er hvad du gør i en relationel DB) ender du med flere identiske navne og skal bruge aliaser.

Indbygget i din funktion

Ved at bruge denne funktion kan du stort set forenkle din FOREACH LOOP til:

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

Hurtigere, dog som en enkelt SQL-sætning med unnest() :

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

Erstatter hele løkken.

Alternativ løsning

Denne variant bygger på adfærden fra UNION ALL med en LIMIT klausul:så snart der er fundet nok rækker, udføres resten aldrig:

  • Måde at prøve flere SELECT'er, indtil et resultat er tilgængeligt?

Med udgangspunkt i dette kan vi outsource INSERT til en separat funktion. Kun dér har vi brug for undtagelseshåndtering. Lige så sikker som den første løsning.

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

Som bruges i hovedfunktionen:

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$;
  • Dette er en smule billigere, hvis de fleste af opkaldene kun behøver SELECT , fordi den dyrere blok med INSERT indeholdende EXCEPTION klausul er sjældent indtastet. Forespørgslen er også enklere.

  • FOR SHARE er ikke muligt her (ikke tilladt i UNION forespørgsel).

  • LIMIT 1 ville ikke være nødvendigt (testet i s. 9.4). Postgres udleder LIMIT 1 fra INTO _tag_id og udføres kun indtil den første række er fundet.



  1. Old Style Oracle Outer Join-syntaks - Hvorfor skal du finde (+) på højre side af lighedstegnet i en Left Outer join?

  2. Transaktioner virker ikke for min MySQL DB

  3. Beregning af medianen med en dynamisk markør

  4. Konfigurer SQL-job i SQL Server ved hjælp af T-SQL