Як використовувати ПОВЕРНЕННЯ з ON CONFLICT у PostgreSQL?


149

У PostgreSQL 9.5 є наступний UPSERT:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Якщо немає конфліктів, він повертає щось подібне:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Але якщо є конфлікти, він не повертає жодних рядків:

----------
    | id |
----------

Я хочу повернути нові idстовпці, якщо немає конфліктів, або повернути існуючі idстовпці конфліктуючих стовпців.
Чи можна це зробити? Якщо так, то як?


1
Використовуйте, ON CONFLICT UPDATEщоб змінити рядок. Тоді RETURNINGце захопить.
Гордон Лінофф

1
@GordonLinoff Що робити, якщо нічого не можна оновити?
Оку

1
Якщо немає нічого для оновлення, це означає, що конфлікту не було, тому він просто вставляє нові значення та повертає їх id
zola

1
Ви знайдете інші способи тут . Я хотів би знати різницю між ними в плані продуктивності.
Станісладрг відновлює Моніку

Відповіді:


88

У мене була точно така ж проблема, і я вирішив її, використовуючи "зробити оновлення" замість "не робити нічого", хоча я не мав нічого оновлювати. У вашому випадку це було б щось подібне:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Цей запит поверне всі рядки, незалежно від того, вони були щойно вставлені або вони існували раніше.


11
Одна з проблем цього підходу полягає в тому, що номер послідовності первинного ключа збільшується при кожному конфлікті (помилкове оновлення), що в основному означає, що у вас може виникнути величезні прогалини в послідовності. Будь-які ідеї, як цього уникнути?
Міща

9
@Mischa: так що? Послідовності ніколи не гарантують, що вони будуть безперервними в першу чергу, і прогалини не мають значення (і якщо вони є, послідовність - це неправильно)
a_horse_with_no_name

24
Я б не радив використовувати це в більшості випадків. Я додав відповідь, чому.
Ервін Брандштеттер

4
Ця відповідь, здається, не відповідає DO NOTHINGаспекту оригінального запитання - для мене, здається, оновлено безконфліктне поле (тут "ім'я") для всіх рядків.
PeterJCLaw

Як було сказано в дуже довгій відповіді нижче, використання "Do Update" для поля, яке не змінилося, не є "чистим" рішенням і може спричинити інші проблеми.
Білл Уортінгтон,

202

В даний час прийнята відповідь здається нормальною для однієї цілі конфлікту, мало конфліктів, невеликих кортежів і жодних тригерів. Це дозволяє уникнути випуску одночасності 1 (див. Нижче) з грубою силою. Просте рішення має свою привабливість, побічні ефекти можуть бути менш важливими.

Однак для всіх інших випадків не оновлюйте однакові рядки без потреби. Навіть якщо ви не бачите різниці на поверхні, є різні побічні ефекти :

  • Це може спрацьовувати тригери, які не повинні бути запущені.

  • Він записує "блокування" невинних "рядків, можливо, несучи витрати на одночасні транзакції.

  • Можливо, рядок здасться новим, хоча він і старий (часова мітка транзакцій)

  • Найголовніше , що в моделі MVCC PostgreSQL нова версія рядків пишеться для кожного UPDATE, незалежно від того, змінилися дані рядків. Це спричиняє штрафну ефективність для самого UPSERT, роздуття таблиці, проміжок індексу, штрафну ефективність за наступні операції на столі, VACUUMвартість. Незначний ефект для декількох дублікатів, але масова для більшості дуп.

Плюс , іноді це не практично або навіть можливо використовувати ON CONFLICT DO UPDATE. Посібник:

Для ON CONFLICT DO UPDATE, conflict_targetповинні бути забезпечені.

Сингл «цільової конфлікт" не представляється можливим , якщо кілька індексів / обмеження залучені.

Ви можете досягти (майже) того ж без порожніх оновлень та побічних ефектів. Деякі з наступних рішень також працюють із 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(що має бути дешевим, оскільки ідеальний індекс за визначенням існує - унікальне обмеження реалізується за допомогою покажчик).

Може бути (набагато) швидшим для багатьох дублікатів. Ефективна вартість додаткових записів залежить від багатьох факторів.

Але побічних ефектів та прихованих витрат у будь-якому випадку менше . Це, швидше за все, дешевше в цілому.

Приєднані послідовності все ще розширені, оскільки значення за замовчуванням заповнюються перед тестуванням на конфлікти.

Про CTE:

При одночасному записі запису

Припускаючи 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, перш ніж повернути повний набір результатів. Цей останній 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 .

Це змушує конкуруючі операції запису чекати до кінця транзакції, коли всі блокування будуть звільнені. Тож будьте короткими.

Детальніше та пояснення:

Тупики?

Захистіть від тупиків , вставляючи рядки в послідовному порядку . Побачити:

Типи даних та касти

Існуюча таблиця як шаблон для типів даних ...

Явні типи типів для першого ряду даних у вільно стоячому 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')
   )
   ...

Це не працює для деяких типів даних. Побачити:

... і імена

Це також працює для всіх типів даних.

Вставляючи всі (провідні) стовпці таблиці, ви можете опускати імена стовпців. Припустимо, що таблиця 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.


2
Ви маєте на увазі, що цей метод не створить прогалин у серіалах, але вони є: ВСТАВКА ... ПРО КОНФЛІКТ НЕ
НЕЩО

1
Мало того, що це не так важливо, але чому це збільшення серіалів? і чи немає способу цього уникнути?
видатний

1
@salient: Як я додав вище: значення за замовчуванням стовпця заповнюються перед тестуванням на конфлікти, і послідовності ніколи не відкочуються назад, щоб уникнути конфліктів із одночасними записами.
Erwin Brandstetter

7
Неймовірно. Працює як шарм і легко зрозуміти, коли ви уважно подивитесь на це. Я все ще бажаю, ON CONFLICT SELECT...де щось, хоча :)
Roshambo

3
Неймовірно. Творці постгресу, здається, мучать користувачів. Чому б просто не зробити зворотний пункт завжди повертати значення, незалежно від того, були вставки чи ні?
Анатолій Алексєєв

16

Upsert, будучи розширенням INSERTзапиту, може бути визначений двома різними способами поведінки у випадку конфлікту обмежень: DO NOTHINGабо DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Зауважте також, що RETURNINGнічого не повертає, тому що кортежі не вставлені . Тепер з DO UPDATEможливим виконувати операції над кортежем, з яким виникає конфлікт. По-перше, зауважте, що важливо визначити обмеження, яке буде використано для визначення конфлікту.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Хороший спосіб завжди отримати ідентифікатор порушеного рядка та дізнатися, чи це вставка чи вставка. Тільки те, що мені було потрібно.
Moby Duck

Це все ще використовується "Do Update", про недоліки якого вже йшлося.
Білл Уортінгтон,

4

Для вставки одного елемента, ймовірно, я б скористався при поверненні ідентифікатора:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

Основна мета використання ON CONFLICT DO NOTHING- уникнути помилки підкидання, але це не спричинить повернення рядків. Тому нам потрібен інший, SELECTщоб отримати наявний ідентифікатор.

У цьому SQL, якщо він не вдасться до конфліктів, він нічого не поверне, тоді другий SELECTотримає існуючий рядок; якщо він успішно вставляється, то буде два однакові записи, тоді нам потрібно UNIONоб'єднати результат.


Це рішення працює добре і уникає зайвих записів (оновлень) у БД !! Приємно!
Саймон С

0

Я змінив дивовижну відповідь Ервіна Брандстеттера, який не збільшуватиме послідовність, а також не записуватиме-блокує жодні рядки. Я відносно новачок у PostgreSQL, тому, будь ласка, повідомте мене, якщо ви бачите якісь недоліки цього методу:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Це передбачає, що таблиця chatsмає унікальне обмеження на стовпці(usr, contact) .

Оновлення: додано запропоновані зміни з Spatar (нижче). Дякую!


1
Замість того, щоб CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsпросто написати r.id IS NOT NULL as row_exists. Замість того, щоб WHERE row_exists=FALSEпросто написати WHERE NOT row_exists.
spatar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.