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

У меня есть следующий 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

У меня была точно такая же проблема, и я решил ее с помощью "do update" вместо "ничего не делать", хотя мне нечего обновлять. В вашем случае это будет примерно так:

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;

Этот запрос вернет все строки, независимо от того, они только что были вставлены или они существовали раньше.

Ответ 2

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

Однако для всех остальных случаев не не нужно обновлять одинаковые строки. Даже если вы не видите разницы на поверхности, существуют различные побочные эффекты:

  • Он может запускать триггеры, которые не должны запускаться.

  • Он записывает блокировки "невинных" строк, возможно, из-за затрат на параллельные транзакции.

  • Может показаться, что строка кажется новой, хотя она и старая (временная метка транзакции).

  • Самое главное. модель PostgreSQL MVCC новая версия строки написана в любом случае, независимо от того, данные строки одинаковы. Это приводит к штрафу за производительность самого UPSERT, раздуванию таблицы, раздуванию индекса, штрафу за производительность для всех последующих операций в таблице, стоимости VACUUM. Небольшой эффект для нескольких дубликатов, но массивный для большинства обманов.

Вы можете добиться одинакового результата без пустых обновлений и побочных эффектов:

WITH input_rows("user", "contact", "name") AS (
   VALUES
      (text 'foo', text 'bar', text 'bob')  -- type casts in first row
    , ('foo1', 'bar1', 'barb')
    -- more?
   )
, ins AS (
   INSERT INTO chats ("user", "contact", "name") 
   SELECT * FROM input_rows
   ON CONFLICT ("user", "contact") DO NOTHING
   RETURNING id
   )
SELECT 'i' AS source, id
FROM   ins                                 -- 'i' for 'inserted'
UNION  ALL
SELECT 's' AS source, c.id                 -- 's' for 'selected'
FROM   input_rows
JOIN   chats c USING ("user", "contact");  -- columns of unique index

Столбец source является необязательным дополнением, чтобы продемонстрировать, как это работает. Возможно, вам действительно нужно это, чтобы рассказать о различии между обоими случаями (другое преимущество перед пустым письмом).

Заключительный JOIN chats работает, потому что недавно вставленные строки из привязанного к изменению данных CTE еще не видны в базовой таблице. (Все части одной и той же команды видят те же снимки базовых таблиц.)

Поскольку выражение VALUES свободно (не привязано непосредственно к INSERT), Postgres не может выводить типы данных из целевых столбцов, и вам, возможно, придется добавлять явные типы приведения. Руководство:

Когда VALUES используется в INSERT, все значения автоматически принуждение к типу данных соответствующего столбца назначения. когда он используется в других контекстах, может потребоваться указать правильный тип данных. Если в записях указаны все литеральные константы, принудительного первого достаточно, чтобы определить предполагаемый тип для всех.

Сам запрос может быть немного дороже для нескольких обманов из-за накладных расходов CTE и дополнительного SELECT (что должно быть дешево, поскольку идеальный индекс существует по определению - уникальное ограничение реализуется с помощью индекс).
Может быть (намного) быстрее для многих дубликатов. Эффективная стоимость дополнительных записей зависит от многих факторов.

Но в любом случае меньше побочных эффектов и скрытых затрат. (Прикрепленные последовательности все еще расширены, поскольку значения по умолчанию заполняются перед тестированием конфликтов.) Это, скорее всего, более дешевое в целом.

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

...
ON CONFLICT ("user", "contact") DO UPDATE
SET name = name WHERE FALSE  -- never executed, only locks rows
...

Подробнее и объяснение:


Кроме того: никогда не используйте цитируемые зарезервированные слова, такие как "user" как идентификатор. Это загруженный нож для ног. Используйте только правовые, строчные, некотируемые идентификаторы.

Существующая таблица в качестве шаблона для типов данных

Явные типы приведения для первой строки данных в свободностоящем выражении VALUES могут быть неудобными. Есть способы обойти это. Вы можете использовать любое существующее отношение (таблица, представление,...) в качестве шаблона строки. Целевая таблица является очевидным выбором для использования. Входные данные принудительно применяются к соответствующим типам, как в предложении VALUES INSERT:

WITH input_rows AS (
  (SELECT "user", contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo' , 'bar' , 'bob')  -- no type casts needed
    , ('foo1', 'bar1', 'barb')
   )
   ...

... и имена

Если вы вставляете целые строки, вы также можете опустить имена столбцов - если хотите. Предполагая, что таблица chats в примере содержит только три столбца:

WITH input_rows AS (
  (TABLE chats LIMIT 0)  -- copy whole row structure
   UNION ALL
   VALUES
      ('foo' , 'bar' , 'bob')  -- no type casts needed
    , ('foo1', 'bar1', 'barb')
   )
   ...

TABLE является просто сокращением синтаксиса для SELECT * FROM здесь.

Подробное объяснение и альтернативы:

Ответ 3

Согласно http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-upsert/:

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)