принятый в настоящее время ответ кажется приемлемым для единственной цели конфликта, нескольких конфликтов, небольших кортежей и отсутствия триггеров. Он позволяет избежать проблемы параллелизма 1 (см. Ниже) с помощью грубой силы. Простое решение имеет свою привлекательность, побочные эффекты могут быть менее важными.
Однако во всех остальных случаях не обновляйте идентичные строки без необходимости. Даже если вы не видите разницы на поверхности, есть различные побочные эффекты:
Это может привести к срабатыванию триггеров, которые не следует запускать.
Он блокирует запись невинных строк, что может повлечь за собой затраты на одновременные транзакции.
Строка может показаться новой, хотя она и старая (отметка времени транзакции).
Самое главное, что с моделью PostgreSQL MVCC a новая версия строки записывается для каждого 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 большую часть времени ничего не делает. Только если в возвращаемом результате пропадают строки, мы используем грубую силу.
Еще больше накладных расходов. Чем больше конфликтов с уже существующими строками, тем больше вероятность, что это превзойдет простой подход.
Один побочный эффект: второй 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
.
person
Erwin Brandstetter
schedule
14.02.2017
ON CONFLICT UPDATE
, чтобы изменить строку. ТогдаRETURNING
захватит это. - person Gordon Linoff   schedule 10.01.2016