Ограничение CHECK — это правило, которому обязана соответствовать каждая строка
Ограничение CHECK в SQLite — это логическое выражение, которое привязывается к таблице. SQLite вычисляет его при каждом INSERT и UPDATE, и если результат оказывается ложным, операция завершается с ошибкой. Так бизнес-правило — например, «цена не может быть отрицательной» или «статус должен быть одним из трёх допустимых значений» — зашивается прямо в схему БД.
Первые две строки спокойно записываются. А вот третья валится с ошибкой CHECK constraint failed — и в таблицу не попадает. Ограничение CHECK срабатывает для любого источника записи: будь то ваше приложение, миграционный скрипт или кто-то, кто решил поковыряться в CLI.
CHECK на уровне столбца и на уровне таблицы
Ограничение CHECK в SQLite можно объявить в двух местах: сразу после описания столбца (на уровне столбца) или после перечисления всех столбцов (на уровне таблицы). Работают они одинаково — выбор зависит лишь от того, как код будет читаться понятнее.
Первое бронирование вставляется без проблем. Второе валится — дата окончания идёт раньше начала. Правила на один столбец лучше выглядят как ограничение уровня столбца, а всё, что сравнивает два и более столбца, удобнее писать на уровне таблицы.
Ограничение значений списком допустимых вариантов
Частый сценарий — заставить столбец принимать только одно из заранее заданных значений. В SQLite нет встроенного типа enum, поэтому стандартный приём — это CHECK ... IN (...):
Третья строка не пройдёт — значения 'pending' нет в списке разрешённых. Если позже понадобится добавить новый статус, таблицу придётся пересоздавать (об этом ниже), так что подумайте пару раз, прежде чем жёстко фиксировать перечень. Но для по-настоящему закрытых наборов значений — ролей пользователей, состояний заказа — это именно то ограничение, которое вам нужно.
Именованное ограничение CHECK
По умолчанию ограничение остаётся безымянным. В сообщении об ошибке вы увидите только CHECK constraint failed и само выражение — терпимо, когда CHECK на таблице один, и сущий ад, когда их пять. Имя задаётся через CONSTRAINT:
Теперь в тексте ошибки есть имя ограничения, и сразу понятно, какое именно правило нарушено. Имя стоит лишних пары символов, а окупится оно в первый же раз, когда что-нибудь сломается в проде.
CHECK и NULL: подводный камень
CHECK срабатывает, когда выражение истинно или NULL. Падает оно только при явном false. Звучит странно, пока не вспомнишь, что почти любое сравнение с NULL даёт в результате NULL, а не true или false.
Строка с NULL спокойно проходит — NULL >= 0 даёт NULL, а не false, поэтому CHECK не срабатывает. Если же вам нужно запретить и отрицательные числа, и пропуски, добавьте NOT NULL рядом с CHECK:
Теперь вставка падает на ограничении NOT NULL ещё до того, как сработает CHECK. Эти два ограничения отлично дополняют друг друга: NOT NULL ловит отсутствие значения, а CHECK — его форму.
Полезные встроенные функции внутри CHECK
В выражении ограничения CHECK SQLite можно использовать почти все встроенные функции. Вот те, что встречаются чаще всего:
Три провала разом: кривой email, слишком короткий username и код страны строчными буквами. LIKE отлично справляется с простыми шаблонами; length(), upper(), lower() и арифметика — всё это тоже идёт в ход. Главное — держите выражение детерминированным: если внутри CHECK сунуть что-то вроде random() или current_timestamp, правило начнёт давать разный результат на разных строках, а это почти всегда не то, что нужно.
CHECK или триггер в SQLite
И CHECK, и триггеры умеют отклонять плохие данные, и новички нередко зависают над выбором. Простое правило:
- CHECK — когда правило зависит только от той строки, которую сейчас пишут. «Этот столбец больше того столбца», «значение в таком-то диапазоне», «строка соответствует шаблону».
- Триггер (а именно
BEFORE INSERT/UPDATEс вызовомRAISE) — когда правило завязано на другие строки, другие таблицы или требует чего-то посложнее одного булевого выражения.
CHECK работает быстрее, проще и виден прямо в схеме — любой, кто открыл CREATE TABLE, сразу видит правило. К триггеру стоит идти только тогда, когда CHECK действительно не вытягивает нужную проверку.
CHECK нельзя убрать через ALTER TABLE
Вот тут начинается единственное шероховатое место. В SQLite нет ALTER TABLE ... DROP CONSTRAINT. Чтобы удалить или поменять CHECK, таблицу приходится пересобирать:
BEGIN;
CREATE TABLE products_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);
INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;
COMMIT;
Оборачивайте всю операцию в транзакцию — если что-то упадёт посередине, база останется в исходном состоянии. Если на перестраиваемую таблицу ссылаются внешние ключи из других таблиц, придётся повозиться чуть дольше: отключить foreign_keys, пересобрать таблицу, включить обратно и проверить целостность. Подробно разберём это в разделе про миграции — он будет дальше по курсу.
Что дальше: ограничение UNIQUE
CHECK проверяет содержимое значений внутри одной строки. Следующее ограничение — UNIQUE — работает на уровне отношений между строками: оно гарантирует, что ни в одном столбце (или наборе столбцов) не окажется двух одинаковых значений. К нему и переходим.
Часто задаваемые вопросы
Что такое ограничение CHECK в SQLite?
CHECK — это логическое выражение, привязанное к таблице, которому должна удовлетворять каждая строка. SQLite вычисляет его при любом INSERT или UPDATE и отклоняет операцию, если выражение оказалось ложным. Это самый простой способ закрепить правило вроде «цена должна быть положительной», не вынося логику в код приложения.
Можно ли в CHECK ссылаться сразу на несколько столбцов?
Да, для этого ограничение нужно объявить на уровне таблицы, а не привязывать к конкретному столбцу. Например, CHECK (start_date <= end_date), прописанный после списка колонок, спокойно работает с обеими. Технически и в столбцовом CHECK можно сослаться на соседнюю колонку, но когда задействовано больше одного поля — табличный вариант читается понятнее.
Почему CHECK в SQLite не срабатывает на NULL?
CHECK считается пройденным, если выражение даёт TRUE или NULL — отбрасывается строка только при явном FALSE. Поэтому CHECK (age >= 0) пропустит NULL в поле age: ведь NULL >= 0 это NULL, а не ложь. Если нужно запретить и NULL, добавьте рядом ограничение NOT NULL.
Как удалить или изменить CHECK в SQLite?
Напрямую — никак. SQLite не поддерживает ALTER TABLE ... DROP CONSTRAINT. Остаётся либо править sqlite_schema через PRAGMA writable_schema (вариант для смелых и рискованный), либо пересобрать таблицу: создать новую с нужными ограничениями, перелить данные, удалить старую и переименовать новую. Если давать ограничениям осмысленные имена, скрипт пересборки потом гораздо проще читать.