ВСТАВКА PostgreSQL с инструкциями

#sql #postgresql #insert

#sql #postgresql #вставить

Вопрос:

Я использую базу данных postgres, и моя проблема включает в себя две таблицы, их упрощенные версии приведены ниже.

 CREATE TABLE events(
    id SERIAL PRIMARY KEY NOT NULL,
    max_persons INTEGER NOT NULL
);
  

и

 CREATE TABLE requests(
    id SERIAL PRIMARY KEY NOT NULL,
    confirmed BOOLEAN NOT NULL,
    creation_time TIMESTAMP DEFAULT NOW(),
    event_id INTEGER NOT NULL /*foreign key*/
);
  

Есть n события, и в каждом событии может быть до events.max_persons участников. Новые запросы должны быть подтверждены и действительны до 30 минут. По истечении этого периода запросы будут игнорироваться, если они не были подтверждены.

Теперь то, что я хочу сделать, это вставить новое request , только когда сумма всех подтвержденных запросов и всех запросов, которые все еще действительны, но не подтверждены, меньше events.max_persons .

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

 SELECT 
    e.id,
    SUM(CASE WHEN r.confirmed = 1 THEN 1 ELSE 0 END) AS number_confirmed
    SUM(CASE WHEN r.creation_time > (CURRENT_TIMESTAMP - INTERVAL '30 MINUTE') AND r.confirmed = 0 THEN 1 ELSE 0 END) AS number_reserved,
    e.max_persons
FROM events e, requests r
WHERE l.id = ? 
    AND r.event_id = e.id             
    AND (r.confirmed = 1 OR r.creation_time > (CURRENT_TIMESTAMP - INTERVAL '30 MINUTE'))
GROUP BY e.id, e.max_persons              
HAVING SUM(CASE WHEN r.confirmed = 1 OR (r.creation_time > (CURRENT_TIMESTAMP - INTERVAL '30 MINUTE')) THEN 1 ELSE 0 END) < e.max_persons";
  

Возможно ли достичь этого с помощью одной команды INSERT?

Ответ №1:

Вы могли бы сделать это следующим образом:

 INSERT INTO requests
   SELECT * FROM (VALUES (...)) row
      WHERE ...
  

и напишите WHERE предложение, которое имеет значение true только в том случае, если выполняется ваше условие.

Но с этим подходом есть фундаментальная проблема, а именно то, что он подвержен условию гонки.

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

Для этого есть два решения:

  • Заблокируйте таблицу перед тестированием и вставкой. Это просто, но очень плохо для параллелизма.

  • Используйте SERIALIZABLE транзакции повсюду. Тогда это должно вызвать ошибку сериализации, и один из операторов должен быть повторен и обнаружит, что условие нарушено, когда это произойдет.

Комментарии:

1. Здравствуйте, извините за поздний ответ. В настоящее время я решил свою проблему, выполнив запрос, который получает разницу между max_persons и number_booked / number_reserved и выполняет оператор insert только в том случае, если он больше 0. Это жизнеспособная практика? И еще один вопрос: если я использую СЕРИАЛИЗУЕМЫЕ транзакции, могу ли я справиться с проблемами, например, когда два человека пытаются зарегистрироваться на одно и то же событие одновременно, но остается только одно место?

2. Ваш текущий метод уязвим для проблемы, которую я указал в своем ответе — если две такие транзакции выполняются параллельно, и только одно место свободно, они оба добавят новый запрос. С SERIALIZABLE этого не могло произойти. Одна из транзакций будет завершена с ошибкой сериализации и должна быть повторена. Во время повторной попытки это покажет, что событие уже забронировано.

3. Спасибо, собираюсь попробовать это!