Learning Platform
Глоссарий Troubleshooting
Урок 05.05 · 18 мин
Начальный
relational-modelconstraintsdata-integrityreferential-integrity

Constraints как часть модели: domain, key, referential, check

Модель данных — это не только список таблиц и столбцов. Это ещё и набор правил, которым данные обязаны подчиняться. «Возраст не может быть отрицательным», «у каждого заказа есть существующий клиент», «email уникален» — всё это часть модели, ничуть не меньшая, чем сами таблицы. В реляционной модели эти правила называются constraints (ограничения целостности), и они формулируются прямо в схеме базы данных.

Ключевая идея урока: constraint, объявленный в схеме, проверяется самой СУБД при каждой записи. Это принципиально надёжнее, чем проверка в коде приложения. Приложений может быть несколько (веб, мобильное, скрипт миграции, ручной SQL аналитика), каждое может содержать баг или просто забыть проверку. СУБД — одна, и она проверяет правило для всех и всегда. Constraint в схеме — это инвариант, который невозможно обойти.

Codd выделял несколько видов ограничений. Разберём четыре, с которыми junior-инженер работает каждый день.

Domain constraint: какие значения допустимы

В первом уроке модуля мы говорили: domain — это множество допустимых значений атрибута. Domain constraint — это и есть правило, ограничивающее столбец только значениями из его domain.

Самый базовый уровень domain constraint — тип данных. Объявив столбец age INTEGER, вы уже запретили в него строку 'hello'. Но тип — грубое приближение: INTEGER разрешает и -5, и 999999, хотя возраст человека такими не бывает.

Domain доуточняют двумя средствами:

  • NOT NULL — запрещает отсутствие значения (мы подробно разбирали в уроке про NULL).
  • CHECK — задаёт произвольный предикат (о нём ниже, это отдельный пункт; CHECK часто и используют именно для уточнения domain).

Некоторые СУБД поддерживают и явный тип DOMAIN или ENUM:

-- PostgreSQL: domain как именованный тип
CREATE DOMAIN currency_code AS CHAR(3)
    CHECK (VALUE IN ('USD', 'EUR', 'RUB', 'JPY'));

CREATE TABLE prices (
    price_id INTEGER PRIMARY KEY,
    amount   NUMERIC(12,2) NOT NULL,
    currency currency_code NOT NULL   -- допустимы только 4 кода валют
);

Domain constraint отвечает на вопрос «какое значение в принципе может стоять в этой ячейке». Это первая линия защиты данных.

Key constraint: уникальность строк

Key constraint гарантирует, что значения в столбце (или наборе столбцов) уникальны — двух строк с одинаковым значением ключа быть не может. Это прямая реализация свойства relation «нет дубликатов строк», о котором говорил второй урок модуля.

Два инструмента в SQL:

  • PRIMARY KEY — главный ключ таблицы. Уникален И автоматически NOT NULL. Таблица может иметь только один PRIMARY KEY.
  • UNIQUE — тоже гарантирует уникальность, но допускает NULL (в большинстве СУБД), и таких ограничений на таблице может быть несколько.
CREATE TABLE users (
    user_id INTEGER PRIMARY KEY,        -- главный ключ: уникален + NOT NULL
    email   TEXT NOT NULL UNIQUE,       -- email тоже уникален (альтернативный ключ)
    phone   TEXT UNIQUE                 -- телефон уникален, но может быть NULL
);

INSERT INTO users VALUES (1, '[email protected]', '+100');
INSERT INTO users VALUES (2, '[email protected]', '+200');
-- ERROR: duplicate key value violates unique constraint
-- СУБД отклонила вставку: email '[email protected]' уже занят

Без key constraint таблица перестаёт быть relation и превращается в «мешок» строк (это мы разбирали). Поэтому key constraint — не опция, а условие реляционности. Всему виду ключей — superkey, candidate key, composite key — посвящён следующий модуль курса; здесь важно зафиксировать: ключ — это constraint, который СУБД проверяет на каждой вставке.

Referential constraint: целостность связей

Referential constraint (он же foreign key, FK) — это правило, связывающее две таблицы. Оно гарантирует: значение в столбце-ссылке одной таблицы обязано существовать как ключ в другой таблице. Это и называется referential integrity (ссылочная целостность).

Классический пример: таблица orders ссылается на users. Не должно существовать заказа, привязанного к несуществующему пользователю — иначе это «осиротевшая» (orphan) строка, мусор, который ломает любую аналитику.

Referential constraint отсекает осиротевшие строки
INSERT заказа с user_id = 99Попытка вставить заказ, ссылающийся на пользователя, которого нет в таблице users.
проверка FK
СУБД ищет user_id = 99 в usersForeign key заставляет СУБД проверить: существует ли строка-родитель. Пользователя 99 нет.
не найдено
INSERT отклонёнОсиротевшая строка не попадает в базу. Без foreign key такой заказ молча сохранился бы и испортил аналитику.
CREATE TABLE users (
    user_id INTEGER PRIMARY KEY,
    name    TEXT NOT NULL
);

CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    user_id  INTEGER NOT NULL REFERENCES users(user_id),  -- foreign key
    amount   NUMERIC(12,2) NOT NULL
);

INSERT INTO users  VALUES (1, 'Ann');
INSERT INTO orders VALUES (101, 1, 250.00);   -- OK: пользователь 1 существует
INSERT INTO orders VALUES (102, 99, 70.00);   -- ERROR: пользователя 99 нет
-- ERROR: insert violates foreign key constraint

Что СУБД физически делает при вставке заказа с FK: она выполняет поиск — есть ли в users строка с таким user_id. Если нет — INSERT отклоняется. Этот поиск стоит денег, и чтобы он был быстрым, столбец FK почти всегда нужно проиндексировать. Без индекса каждая проверка FK — полное сканирование родительской таблицы.

Referential constraint работает в обе стороны. При удалении строки из users СУБД проверяет: не ссылается ли на неё кто-то из orders? Поведение задаётся опциями ON DELETE / ON UPDATE:

ОпцияЧто происходит при удалении родительской строки
ON DELETE RESTRICT (по умолчанию)удаление запрещается, если есть ссылающиеся строки
ON DELETE CASCADEвместе с родителем удаляются все ссылающиеся строки
ON DELETE SET NULLв ссылающихся строках FK становится NULL
-- При удалении пользователя удалить и все его заказы:
CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    user_id  INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
    amount   NUMERIC(12,2) NOT NULL
);

CASCADE удобен, но опасен: одно DELETE FROM users WHERE user_id = 1 может молча стереть тысячи заказов. Выбор опции ON DELETE — это решение модели, и его надо принимать осознанно.

WARNING

В больших аналитических warehouse foreign keys часто объявляют как NOT ENFORCED (декларативно, без проверки) или не объявляют вовсе: проверка FK на каждой загрузке миллиардов строк слишком дорога, а данные уже очищены на предыдущем слое. Но в OLTP-базе, которая принимает запись от пользователей, FK обязателен — там это единственная защита от осиротевших строк.

Check constraint: произвольное бизнес-правило

Check constraint задаёт произвольный логический предикат, которому обязана удовлетворять каждая строка. Это самый гибкий вид constraint: в нём можно выразить почти любое правило об одной строке.

CREATE TABLE products (
    product_id  INTEGER PRIMARY KEY,
    name        TEXT NOT NULL,
    price       NUMERIC(12,2) NOT NULL CHECK (price >= 0),
    discount    NUMERIC(12,2) CHECK (discount >= 0 AND discount <= price),
    status      TEXT NOT NULL CHECK (status IN ('draft', 'active', 'archived'))
);

INSERT INTO products VALUES (1, 'Book', -10, 0, 'active');
-- ERROR: violates check constraint -- price не может быть отрицательной

INSERT INTO products VALUES (2, 'Pen', 100, 150, 'active');
-- ERROR: violates check constraint -- скидка больше цены

CHECK позволяет закодировать в схеме то, что иначе жило бы только в коде приложения: «цена неотрицательна», «дата окончания не раньше даты начала», «скидка не превышает цену». Каждое такое правило, перенесённое в CHECK, — это правило, которое уже невозможно нарушить ни одним приложением.

Ограничение CHECK: стандартный CHECK проверяет предикат в пределах одной строки. Правило «сумма заказа равна сумме его позиций» затрагивает несколько строк из двух таблиц — обычный CHECK так не умеет; для межтабличных и межстрочных правил используют триггеры или проверки в приложении. Но всё, что касается одной строки, должно жить в CHECK.

Тонкость с NULL: CHECK считается не нарушенным, если предикат дал UNKNOWN (то есть если в нём участвовал NULL). CHECK (price >= 0) пропустит строку с price = NULL. Если значение обязательно — добавляйте отдельно NOT NULL.

Четыре вида constraints: на что отвечает каждый
Domain constraintТип данных, NOT NULL, ENUM. Отвечает: какое значение допустимо в этой ячейке.
Key constraintPRIMARY KEY, UNIQUE. Отвечает: уникальна ли строка по этому ключу.
Referential constraintFOREIGN KEY. Отвечает: существует ли строка, на которую идёт ссылка.
Check constraintCHECK с предикатом. Отвечает: соблюдено ли бизнес-правило в пределах строки.
все вместе
Целостность данных гарантирована СУБДConstraints в схеме проверяются СУБД при каждой записи — инвариант, который не обойти ни одним приложением.

Почему constraints — часть модели, а не «настройка БД»

Главная мысль урока. Начинающие воспринимают constraints как технические «галочки» при создании таблицы. Это неверно. Constraint — это записанное знание о предметной области.

«Возраст неотрицателен», «у заказа есть клиент», «email уникален» — это факты о том, как устроен реальный мир, который моделирует база. Если эти факты не записаны в схеме, они существуют только в голове разработчика и в коде приложения — то есть могут быть нарушены: новым приложением, миграцией, ручным UPDATE, багом. Constraint переносит факт из «договорённости» в «инвариант, проверяемый машиной».

Поэтому модель данных без constraints — неполная модель. Она описывает структуру (какие таблицы), но не описывает правила (каким данные обязаны быть). Грамотное проектирование — это всегда таблицы плюс constraints вместе.

PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE в PostgreSQL ACID: как СУБД гарантирует constraints при конкурентных изменениях

Попробуй сам

Спроектируйте и создайте в PostgreSQL или SQLite две связанные таблицы — authors(author_id, name, birth_year) и books(book_id, author_id, title, price, pages) — и заложите в схему как можно больше правил предметной области:

  1. Key: PRIMARY KEY в обеих таблицах; сделайте name автора UNIQUE.
  2. Referential: books.author_id — foreign key на authors. Выберите осознанно опцию ON DELETE: что должно случиться с книгами при удалении автора — запрет, каскад или SET NULL? Обоснуйте выбор.
  3. Check: price > 0, pages > 0, birth_year в разумном диапазоне (например, от 1400 до 2026).
  4. Domain: где уместно, добавьте NOT NULL.
  5. Проверьте каждый constraint «на излом»: вставьте книгу с несуществующим author_id, с отрицательной ценой, с нулевыми страницами, дубликат имени автора. Убедитесь, что СУБД отклоняет каждую некорректную вставку. Это и есть ощущение того, что модель защищает сама себя.

Проверка знанийKnowledge check
Почему правило целостности, записанное как constraint в схеме базы данных, надёжнее, чем то же правило, проверяемое только в коде приложения?
ОтветAnswer
Constraint, объявленный в схеме, проверяется самой СУБД при каждой операции записи — INSERT, UPDATE — и нарушающая правило операция просто отклоняется. Это надёжнее проверки в коде приложения по нескольким причинам. Во-первых, приложений, работающих с одной базой, обычно несколько: веб-бэкенд, мобильный бэкенд, скрипты миграции, ad-hoc запросы аналитиков, фоновые задачи. Каждое из них пришлось бы снабдить одинаковой проверкой, и любое могло бы её забыть или содержать баг. Во-вторых, данные можно изменить в обход приложения вообще — прямым SQL-запросом или миграцией, и тогда проверка в коде приложения просто не сработает. СУБД же одна, и она применяет constraint для всех клиентов и всех способов доступа без исключений. Поэтому constraint в схеме — это инвариант данных, который невозможно обойти: правило перестаёт быть договорённостью, которую надо помнить, и становится свойством данных, гарантированным машиной. Именно поэтому constraints считаются полноценной частью модели данных, а не технической настройкой.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что гарантирует referential constraint (foreign key)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5