PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Дали SELECT или INSERT във функция е склонна към условия на състезание?

Това е повтарящият се проблем на 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 и се изпълнява само докато не бъде намерен първият ред.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Предимствата на PostgreSQL

  2. Опашка за задания като SQL таблица с множество потребители (PostgreSQL)

  3. Поведение на NOT LIKE със стойности NULL

  4. За какво се използват '$$' в PL/pgSQL

  5. Удостоверяването с парола за Postgres е неуспешно