В даний час прийнята відповідь здається нормальною для однієї цілі конфлікту, мало конфліктів, невеликих кортежів і жодних тригерів. Це дозволяє уникнути випуску одночасності 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
.
ON CONFLICT UPDATE
щоб змінити рядок. ТодіRETURNING
це захопить.