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

Как да използвам RETURNING с ON CONFLICT в PostgreSQL?

Приетият в момента отговор изглежда добре за единична цел за конфликт, няколко конфликта, малки кортежи и без тригери. Той избягва проблем с паралелност 1 (виж по-долу) с груба сила. Простото решение има своята привлекателност, страничните ефекти може да са по-малко важни.

За всички останали случаи обаче, не праветене актуализирайте идентични редове без нужда. Дори и да не виждате разлика на повърхността, има различни странични ефекти :

  • Може да задейства тригери, които не трябва да се задействат.

  • Той заключва "невинни" редове, което може да доведе до разходи за едновременни транзакции.

  • Това може да направи реда да изглежда нов, въпреки че е стар (клеймо за време на транзакцията).

  • Най-важното , с MVCC модела на PostgreSQL се пише нов ред за всяка UPDATE , без значение дали данните в реда са се променили. Това води до намаляване на производителността за самия UPSERT, раздуване на таблицата, раздуване на индекса, наказание за изпълнение за последващи операции на таблицата, VACUUM цена. Незначителен ефект за няколко дублирания, но огромен предимно за измамници.

Плюс , понякога не е практично или дори възможно да се използва ON CONFLICT DO UPDATE . Ръководството:

За ON CONFLICT DO UPDATE , conflict_target трябва да бъде предоставена.

единична "конфликтна цел" не е възможна, ако са включени множество индекси/ограничения. Но ето едно свързано решение за множество частични индекси:

  • UPSERT въз основа на ограничение UNIQUE с NULL стойности

Обратно на темата, можете да постигнете (почти) същото без празни актуализации и странични ефекти. Някои от следните решения работят и с ON CONFLICT DO NOTHING (без „цел за конфликт“), за да хванете всички възможни конфликти, които могат да възникнат – които може да са желателни или не.

Без едновременно натоварване на запис

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

source колоната е незадължително допълнение, за да демонстрира как работи това. Всъщност може да ви е необходим, за да разберете разликата между двата случая (друго предимство пред празните записи).

Последните JOIN chats работи, защото нововмъкнатите редове от прикачен модифициращ данни CTE все още не се виждат в основната таблица. (Всички части на един и същ SQL израз виждат едни и същи моментни снимки на основните таблици.)

Тъй като VALUES изразът е самостоятелен (не е директно прикрепен към INSERT ) Postgres не може да извлича типове данни от целевите колони и може да се наложи да добавите изрични прехвърляния на типове. Ръководството:

Когато VALUES се използва в INSERT , всички стойности се принуждават автоматично към типа данни на съответната колона дестинация. Когато се използва в други контексти, може да се наложи да посочите правилния тип данни. Ако всички записи са цитирани литерални константи, принуждаването на първата е достатъчно, за да се определи предполагаемият тип за всички.

Самата заявка (без да се броят страничните ефекти) може да е малко по-скъпа за няколко измамници, поради режийните разходи на CTE и допълнителния SELECT (което би трябвало да е евтино, тъй като перфектният индекс е налице по дефиниция – с индекс се прилага уникално ограничение).

Може да е (много) по-бързо за много дубликати. Ефективната цена на допълнителните записи зависи от много фактори.

Но имапо-малко странични ефекти и скрити разходи във всеки случай. Най-вероятно е по-евтино като цяло.

Прикачените поредици все още са напреднали, тъй като стойностите по подразбиране се попълват преди тестване за конфликти.

Относно CTEs:

  • Заявките от тип SELECT ли са единственият тип, който може да бъде вложен?
  • Дуплициране на изразите SELECT в релационно деление

С едновременно натоварване на запис

Ако приемем, че по подразбиране READ COMMITTED изолация на транзакциите. Свързано:

  • Едновременните транзакции водят до състояние на състезание с уникално ограничение за вмъкване

Най-добрата стратегия за защита срещу условията на състезанието зависи от точните изисквания, броя и размера на редовете в таблицата и в UPSERT, броя на едновременните транзакции, вероятността от конфликти, наличните ресурси и други фактори...

Проблем с паралелност 1

Ако едновременна транзакция е записала в ред, който вашата транзакция сега се опитва да UPSERT, вашата транзакция трябва да изчака другата да завърши.

Ако другата транзакция завърши с ROLLBACK (или каквато и да е грешка, т.е. автоматично ROLLBACK ), вашата транзакция може да продължи нормално. Незначителен възможен страничен ефект:пропуски в последователни номера. Но няма липсващи редове.

Ако другата транзакция приключи нормално (неявно или явно COMMIT ), вашият INSERT ще открие конфликт (UNIQUE индекс/ограничението е абсолютно) и DO NOTHING , следователно също не връща реда. (Също така не може да се заключи редът, както е показано в проблем с паралелност 2 по-долу, тъй като не се вижда .) SELECT вижда същата моментна снимка от началото на заявката и също така не може да върне все още невидимия ред.

Всички такива редове липсват в набора от резултати (въпреки че съществуват в основната таблица)!

Товаможе да е добре както е . Особено ако не връщате редове като в примера и сте доволни, знаейки, че редът е там. Ако това не е достатъчно добро, има различни начини да го заобиколите.

Можете да проверите броя на редовете на изхода и да повторите изявлението, ако не съвпада с броя на редовете на входа. Може да е достатъчно добър за редкия случай. Въпросът е да стартирате нова заявка (може да бъде в същата транзакция), която след това ще види новосъздадените редове.

Или проверете за липсващи редове с резултати вътре същата заявка и презаписване тези с трика с груба сила, демонстриран в отговора на Алекстони.

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

Това е като заявката по-горе, но добавяме още една стъпка с CTE ups , преди да върнем complete набор от резултати. Последният CTE няма да направи нищо през повечето време. Само ако редовете липсват от върнатия резултат, ние използваме груба сила.

Още повече режийни. Колкото повече конфликти с вече съществуващи редове, толкова по-вероятно е това да надмине простия подход.

Един страничен ефект:2-рият UPSERT записва редове неправилно, така че въвежда отново възможността за блокиране (вижте по-долу), ако три или повече транзакциите, записващи в едни и същи редове, се припокриват. Ако това е проблем, имате нужда от различно решение - като повторение на цялото изявление, както е споменато по-горе.

Проблем с паралелност 2

Ако едновременните транзакции могат да пишат в участващи колони от засегнати редове и трябва да се уверите, че редовете, които сте намерили, все още са там на по-късен етап в същата транзакция, можете да заключите съществуващи редове евтино в CTE ins (което иначе би се отключило) с:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

И добавете заключваща клауза към SELECT също като FOR UPDATE .

Това кара конкуриращите се операции на запис да чакат до края на транзакцията, когато всички заключвания бъдат освободени. Така че бъдете кратки.

Повече подробности и обяснения:

  • Как да включите изключени редове в ВРЪЩАНЕ от INSERT... ON CONFLICT
  • Изберете или INSERT във функция предразположени ли са условия на състезание?

Застой?

Защитете отзастойте чрез вмъкване на редове в последователен ред . Вижте:

  • Застой с многоредови INSERT, въпреки че ПРИ КОНФЛИКТ НЕ ПРАВЕТЕ НИЩО

Типове данни и прехвърляния

Съществуваща таблица като шаблон за типове данни...

Изричен тип прехвърля за първия ред данни в свободно стоящите VALUES изразяването може да е неудобно. Има начини за заобикаляне. Можете да използвате всяка съществуваща връзка (таблица, изглед, ...) като шаблон на ред. Таблицата на целта е очевидният избор за случая на употреба. Входните данни се принуждават към подходящи типове автоматично, като в VALUES клауза на INSERT :

WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

Това не работи за някои типове данни. Вижте:

  • Прехвърляне на NULL тип при актуализиране на няколко реда

... и имена

Това също работи за всички типове данни.

Докато вмъквате във всички (водещи) колони на таблицата, можете да пропуснете имената на колоните. Да приемем, че chats в примера се състои само от 3 колони, използвани в UPSERT:

WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

Настрана:не използвайте запазени думи като "user" като идентификатор. Това е заредена пушка. Използвайте законни идентификатори с малки букви, които не са в кавички. Замених го с usr .



  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. Преобразувайте името на месеца в номер на месеца в PostgreSQL

  3. Как Acos() работи в PostgreSQL

  4. Функции на прозореца или общи таблични изрази:пребройте предишните редове в рамките на диапазона

  5. UUID или SEQUENCE за първичен ключ?