Получить идентификатор из условного INSERT

Для такой таблицы:

CREATE TABLE Users(
    id SERIAL PRIMARY KEY,
    name TEXT UNIQUE
);

Какой будет правильная вставка с одним запросом для следующей операции:

Учитывая пользователя name, вставьте новую запись и верните новый id. Но если name уже существует, просто верните id.

Я знаю о новом синтаксисе в PostgreSQL 9.5 для ON CONFLICT(column) DO UPDATE/NOTHING, но я не могу понять, как он может помочь, если вообще поможет, учитывая, что мне нужно вернуть id.

Кажется, что RETURNING id и ON CONFLICT не принадлежат друг другу.


person vitaly-t    schedule 18.03.2016    source источник
comment
Эта логика действительно должна быть в базе данных?   -  person Krismorte    schedule 18.03.2016
comment
@ErwinBrandstetter это кажется сложнее, чем я думал. Я пытался выяснить, следует ли использовать решение с одним запросом в приложении Node.js или просто придерживаться SELECT id FROM users WHERE name='text', проверить, найдено ли, а если нет, то выполнить INSERT..., что составляет 2 запроса, но теперь это кажется проще, чем вариант с одним запросом. Я не ожидал, что это будет так неловко. Я надеялся, что новый ON CONFLICT поможет, но, увы, нет. Спасибо за ссылки!   -  person vitaly-t    schedule 18.03.2016
comment
Дело становится чрезвычайно сложным, как только в игру вступает параллельный доступ для записи. Я добавлю еще один ответ.   -  person Erwin Brandstetter    schedule 18.03.2016
comment
@ErwinBrandstetter, казалось бы, самое простое решение - использовать транзакцию и выполнить два отдельных запроса - выбрать + вставить. Это то, что я использую в данный момент.   -  person vitaly-t    schedule 18.03.2016
comment
Я думаю, что нашел лучшее решение.   -  person Erwin Brandstetter    schedule 18.03.2016
comment
Отвечает ли это на ваш вопрос? ВЫБРАТЬ или ВСТАВИТЬ строку одной командой   -  person Bergi    schedule 04.12.2019


Ответы (2)


Реализация UPSERT чрезвычайно сложна для защиты от одновременного доступа к записи. Взгляните на эту вики Postgres, которая служила журналом во время первоначальной разработки. Хакеры Postgres решили не включать «исключенные» строки в предложение RETURNING для первого выпуска Postgres 9.5. Они могут что-то добавить для следующего релиза.

Это важное утверждение в руководстве, объясняющее вашу ситуацию:

Синтаксис списка RETURNING идентичен выходному списку SELECT. Будут возвращены только строки, которые были успешно вставлены или обновлены. Например, если строка была заблокирована, но не обновлена ​​из-за невыполнения условия пункта ON CONFLICT DO UPDATE ... WHERE, строка не будет возвращена.

Жирный акцент мой.

Чтобы вставить одну строку:

Без одновременной загрузки записи в одну и ту же таблицу

WITH ins AS (
   INSERT INTO users(name)
   VALUES ('new_usr_name')         -- input value
   ON     CONFLICT(name) DO NOTHING
   RETURNING users.id
   )
SELECT id FROM ins
UNION  ALL
SELECT id FROM users          -- 2nd SELECT never executed if INSERT successful
WHERE  name = 'new_usr_name'  -- input value a 2nd time
LIMIT  1;

С возможной параллельной записью в таблицу

Consider this instead (for single row INSERT):

Чтобы вставить набор строк:

Все три с очень подробным объяснением.

person Erwin Brandstetter    schedule 18.03.2016
comment
Видите ли вы какую-либо проблему в более раннем ответе Клодоальдо Нето? Что мне в нем нравится, так это то, что для его работы не требуется PostgreSQL 9.5. - person vitaly-t; 18.03.2016
comment
@vitaly-t: Это серьезное заявление, но оно не распространяется на упомянутое состояние гонки. Это также, вероятно, дороже. Вы можете добавить LIMIT 1 из моего заявления для производительности. - person Erwin Brandstetter; 18.03.2016
comment
Спасибо за ваш ответ. Мое предыдущее исследование было сформулировано здесь: github.com /vitaly-t/pg-promise/blob/master/examples/, и я надеялся, что смогу улучшить его с помощью альтернативы с одним запросом. Но теперь, учитывая всю сложность этого, я не совсем уверен, можно ли считать это улучшением. Но, по крайней мере, хорошо знать, что это возможно. - person vitaly-t; 18.03.2016
comment
@vitaly-t: Вы можете добавить FOR SHARE или FOR KEY SHARE к подзапросу EXISTS в ответе Клодоальдо, чтобы покрыть состояние гонки в более старых версиях. Или сравните с ON CONFLICT ... DO NOTHING, чтобы сравнить яблоки с яблоками. Кроме того, глядя на вашу реализацию на github, я бы рассмотрел функции на стороне сервера, подробно описанные в моем связанном ответе, которая превосходит несколько поездок туда и обратно на сервер БД в вашем подходе. - person Erwin Brandstetter; 18.03.2016
comment
Я бы опубликовал это как комментарий, но это было бы слишком долго, поэтому я изменил свой ответ. - person Clodoaldo Neto; 18.03.2016
comment
@vitaly-t: обратите внимание на обновления. Возможный угловой случай не был покрыт. - person Erwin Brandstetter; 07.05.2020

Для вставки одной строки и без обновления:

with i as (
    insert into users (name)
    select 'the name'
    where not exists (
        select 1
        from users
        where name = 'the name'
    )
    returning id
)
select id
from users
where name = 'the name'

union all

select id from i

Руководство по основному и with подзапросам части:

Основной запрос и запросы WITH (условно) выполняются одновременно.

Хотя для меня это звучит как «тот же снимок», я не уверен, так как не знаю, что означает теоретически в этом контексте.

Но также есть:

Подоператоры в WITH выполняются одновременно друг с другом и с основным запросом. Таким образом, при использовании операторов, изменяющих данные, в WITH порядок, в котором на самом деле происходят указанные обновления, непредсказуем. Все операторы выполняются с одним и тем же моментальным снимком

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

with s as (
    select id
    from users
    where name = 'the name'
), i as (
    insert into users (name)
    select 'the name'
    where not exists (select 1 from s)
    returning id
)
select id from s
union all
select id from i
person Clodoaldo Neto    schedule 18.03.2016
comment
Тот факт, что CTE основаны на одном и том же моментальном снимке, не может предотвратить состояние гонки. Один пример: представьте, что две транзакции начинаются практически в один и тот же момент. Оба обнаруживают, что «имя» еще не существует, и пытаются INSERT. Вы получаете уникальное нарушение. Есть веские причины для нового INSERT ... ON CONFLICT ... - person Erwin Brandstetter; 19.03.2016
comment
@ErwinBrandstetter Я не уверен, что то, что вы описываете, является точным, когда вы используете слово transaction, что подразумевает использование begin и commit. В таком случае вторая транзакция будет заблокирована до завершения первой по самому определению транзакций. - person vitaly-t; 19.03.2016
comment
@Erwin: Теперь я понимаю твое мышление. Насколько я понимаю, эти две транзакции — это просто транзакции. Один из них потерпит неудачу с правильным уникальным сообщением о нарушении, которое будет поймано и обработано приложением. Это не похоже на вводящий в заблуждение результат. Я думаю, что попытка избежать сбоя второй транзакции — это перебор. Незначительный выигрыш по сравнению с добавленной сложностью. - person Clodoaldo Neto; 19.03.2016
comment
@Clodoaldo: Вот что такое UPSERT (или INSERT или SELECT): чтобы избежать ошибки и получить желаемый результат за один шаг. И случай, о котором я упомянул, — лишь одно из нескольких возможных осложнений. - person Erwin Brandstetter; 19.03.2016
comment
@vitaly-t: В Postgres каждый оператор DML выполняется внутри транзакции. Большинство клиентов работают в режиме автоматической фиксации. BEGIN и COMMIT необязательны для переноса нескольких операторов в одну транзакцию. Транзакции не заблокированы. Ресурсы заблокированы, что может блокировать транзакции. Блокировки приобретаются по пути и освобождаются в конце транзакции. Параллельные транзакции могут запускать одни и те же SELECT и обнаруживать, что одна и та же строка отсутствует. Блокировки отсутствующих строк в Postgres невозможны. Несколько человек могут попытаться INSERT использовать один и тот же ключ, и только один из них будет успешным с уникальным индексом. - person Erwin Brandstetter; 19.03.2016
comment
@ErwinBrandstetter спасибо, теперь я вижу, что использование транзакций в этом случае совсем не помогает, поэтому я исправил свой пример здесь: github.com/vitaly-t/pg-promise/blob/master/examples/ - person vitaly-t; 19.03.2016