В настоящее время принятый ответ кажется приемлемым для одной цели конфликта, нескольких конфликтов, небольших кортежей и отсутствия триггеров. Это позволяет избежать проблемы параллелизма 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 ничего не сделает большую часть времени. Только если строки возвращаются из возвращаемого результата, мы используем грубую силу.
Еще больше накладных расходов. Чем больше конфликтов с уже существующими строками, тем больше вероятность, что это превзойдет простой подход.
Один побочный эффект: второй 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
поймаете это.