В системе бронирований номер помещения должен быть уникальным только среди активных бронирований, а завершённые записи могут повторять его. Какое решение на уровне схемы надёжно выразит это правило?
Используйте условный уникальный индекс: уникальность должна применяться только к строкам с активным статусом. Обычное ограничение UNIQUE не подходит, потому что оно проверяет все строки, включая завершённые бронирования.
Такое решение переносит бизнес-правило в базу данных и защищает его от гонок между параллельными транзакциями. Важно учитывать, что условная уникальность обычно реализуется как возможность конкретной СУБД, а не как полностью переносимое стандартное ограничение SQL.
Обычное ограничение UNIQUE решает задачу глобальной уникальности значения или комбинации значений во всей таблице. На практике часто требуется более узкое правило: уникальность действует только для актуальных, опубликованных или неудалённых строк.
Для таких случаев появились частичные или фильтрованные индексы. Они позволяют индексировать только подмножество строк и проверять уникальность именно в нём, не заставляя удалять исторические данные.
Предположим, таблица хранит и текущие, и завершённые бронирования. Требование состоит в том, что одно помещение может иметь только одно активное бронирование, но после завершения бронирования его номер может использоваться снова.
Обычный UNIQUE на номере помещения запретит повторное использование номера даже в завершённых записях. Проверка в приложении ненадёжна: два параллельных запроса могут одновременно убедиться, что активной записи нет, а затем оба вставить новую строку.
Условный уникальный индекс включает в проверяемое множество только строки, удовлетворяющие условию активного состояния. Поэтому две завершённые записи могут иметь один номер помещения, но две активные записи с таким номером вызовут конфликт уникальности.
В этом примере индекс содержит только активные бронирования. Уникальность гарантируется самой базой данных, поэтому она сохраняется при вставках и изменениях статуса, включая конкурентные транзакции.
Есть важные ограничения. Синтаксис и возможности зависят от СУБД: например, PostgreSQL поддерживает частичный индекс с предикатом, SQL Server — фильтрованный уникальный индекс, а в других системах может потребоваться вычисляемый признак или иной эквивалент. Условие должно быть определено однозначно: если активность выражается несколькими статусами, их нужно включить в предикат индекса.
Индекс не заменяет обычные ограничения NOT NULL, CHECK или внешний ключ. Например, отдельная проверка должна гарантировать допустимые значения статуса, иначе некорректный статус может просто исключить строку из условного индекса и обойти задуманное правило.
В сервисе аренды помещения хранились все бронирования, включая отменённые. Сначала приложение перед созданием бронирования выполняло поиск активной записи, но при одновременных запросах двух операторов возникали дубли.
Рассматривались три варианта. Удалять старые записи было нельзя из-за аудита. Блокировка помещения в приложении усложняла транзакции и всё равно требовала аккуратной настройки. Обычный уникальный индекс сохранял бы конфликт между историческими записями.
Был выбран условный уникальный индекс по номеру помещения для активных строк. История сохранилась, конкурентные вставки стали атомарно конфликтовать на уровне базы данных, а приложение получило понятную ошибку уникальности и повторило операцию после выбора другого времени.
Почему нельзя надёжно заменить условный уникальный индекс проверкой в приложении?
Проверка и последующая вставка — это две отдельные операции. Между ними другая транзакция может вставить строку с тем же активным номером. Только ограничение или индекс, проверяемый самой СУБД в рамках операции изменения данных, надёжно устраняет эту гонку; при необходимости дополнительно учитываются уровень изоляции и обработка конфликта.
Что произойдёт, если активную запись перевести в завершённое состояние?
После изменения статуса строка перестанет удовлетворять условию индекса и будет исключена из его уникального множества. Номер помещения станет доступен для другого активного бронирования, хотя прежняя строка останется в таблице. Если изменение статуса и создание новой записи должны быть неделимыми, их выполняют в одной транзакции.
Почему условный уникальный индекс не всегда называют ограничением UNIQUE?
UNIQUE — декларативное ограничение, обычно применяемое ко всем строкам, а условный индекс — индекс с предикатом, который одновременно обеспечивает уникальность только выбранного подмножества. Это различие влияет на переносимость, представление объекта в системном каталоге и доступные операции управления. При проектировании нужно проверить документацию конкретной СУБД и не предполагать одинаковый синтаксис в разных диалектах SQL.