Как использовать RETURNING с ON CONFLICT в PostgreSQL?


149

У меня есть следующий UPSERT в PostgreSQL 9.5:

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
Если обновлять нечего, это означает, что конфликта не было, поэтому он просто вставляет новые значения и возвращает их идентификатор
zola

1
Вы найдете другие пути здесь . Хотелось бы узнать разницу между этими двумя показателями с точки зрения производительности.
Stanislasdrg Восстановить Монику

Ответы:


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
@ Миша: ну и что? Последовательности никогда не гарантируют отсутствие пробелов, и пробелы не имеют значения (и если они имеют значение, последовательность будет неправильной)
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 ничего не сделает большую часть времени. Только если строки возвращаются из возвращаемого результата, мы используем грубую силу.

Еще больше накладных расходов. Чем больше конфликтов с уже существующими строками, тем больше вероятность, что это превзойдет простой подход.

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

7
Невероятный. Работает как шарм и легко понять, когда вы внимательно посмотрите на него. Я все еще хочу, ON CONFLICT SELECT...где вещь, хотя :)
Рошамбо

3
Невероятный. Создатели Postgres, кажется, мучают пользователей. Почему бы просто не заставить возвращаемое предложение всегда возвращать значения, независимо от того, были вставки или нет?
Анатолий Алексеев

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

Для вставки одного элемента, я бы, вероятно, использовал бы coalesce при возврате id:

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объединить результат.


Это решение работает хорошо и позволяет избежать ненужной записи (обновления) в БД !! Ницца!
Саймон C

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.
спат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.