22 августа 2026 г. · 2 мин чтения
Двойное бронирование лечится не проверкой, а индексом
В боте бронирования столиков задача выглядит простой: не дать занять один столик дважды на одно и то же время.
Первое решение приходит само. Перед записью спрашиваем базу, свободен ли слот; если занят — отказываем.
Оно неверное, и неверное незаметно. Между запросом и вставкой проходит время. Двое гостей, нажавших кнопку почти одновременно, оба получат ответ «свободно», и оба запишутся. Ошибка не воспроизводится в спокойный день и обязательно случится в пятницу вечером, когда бронируют одновременно.
Правильное место для запрета — сама база. Уникальный индекс по столику, дате и времени физически не даст записать вторую строку: вторая вставка упадёт с ошибкой уникальности, и её остаётся поймать и показать человеку понятное сообщение.
Тонкость здесь одна — отменённые брони. Если включить их в индекс, отменённый столик нельзя будет забронировать заново: строка-то осталась. Решает частичный индекс: условие WHERE исключает отменённые из проверки, оставляя их в таблице.
Общее правило, которое из этого следует: если запрет должен соблюдаться всегда, ему место в базе, а не в коде приложения. Код выполняется в нескольких экземплярах и в произвольном порядке; уникальный индекс — в одном месте и без исключений.
-- Один столик нельзя занять дважды на один и тот же слот.-- Отменённые брони из проверки исключаются.---- Проверять занятость запросом перед вставкой ненадёжно: между-- проверкой и записью успевает вклиниться второй гость. Здесь-- запрет живёт в самой базе, и обойти его нельзя.CREATE UNIQUE INDEX IF NOT EXISTS idx_booking_slot ON bookings (table_id, book_date, book_time) WHERE status <> 'cancelled';