Ограничения и первичный ключ

Заставляем базу следить за корректностью данных с помощью ограничений.

Ограничение (constraint) — это правило на уровне таблицы, которое база проверяет автоматически и не даёт записать данные, нарушающие его.

PRIMARY KEY и rowid

Первичный ключ однозначно определяет строку. В SQLite у каждой обычной таблицы есть скрытый целочисленный столбец rowid. Если вы объявляете столбец как INTEGER PRIMARY KEY, он становится псевдонимом rowid — самым эффективным ключом, который к тому же автоматически нумеруется.

AUTOINCREMENT vs обычный rowid

Без ключевого слова AUTOINCREMENT SQLite нумерует строки сам: новый id обычно равен «максимальный + 1». Но если удалить последнюю строку, её номер может быть переиспользован. Слово AUTOINCREMENT запрещает повторное использование номеров — они только растут.

CREATE TABLE logs (
    id  INTEGER PRIMARY KEY AUTOINCREMENT,
    msg TEXT
);

INSERT INTO logs (msg) VALUES ('старт'), ('работа');
DELETE FROM logs WHERE id = 2;
INSERT INTO logs (msg) VALUES ('после удаления');

SELECT id, msg FROM logs;

Вывод:

1|старт
3|после удаления

Новая строка получила id = 3, а не 2: благодаря AUTOINCREMENT освободившийся номер не используется повторно. В большинстве случаев AUTOINCREMENT не нужен — обычный INTEGER PRIMARY KEY и проще, и быстрее.

NOT NULL, UNIQUE, DEFAULT, CHECK

Остальные ограничения объявляют рядом со столбцом:

ОграничениеЧто гарантирует
NOT NULLзначение обязательно, NULL запрещён
UNIQUEзначения не повторяются
DEFAULT xзначение по умолчанию, если не указано
CHECK (условие)значение должно удовлетворять условию

Соберём «защищённую» таблицу аккаунтов: email обязателен и уникален, баланс по умолчанию 0 и не может быть отрицательным.

CREATE TABLE accounts (
    id      INTEGER PRIMARY KEY,
    email   TEXT UNIQUE NOT NULL,
    balance INTEGER DEFAULT 0 CHECK (balance >= 0)
);

INSERT INTO accounts (email) VALUES ('[email protected]');
INSERT INTO accounts (email, balance) VALUES ('[email protected]', 500);

SELECT id, email, balance FROM accounts;

Вывод:

1|[email protected]|0
2|[email protected]|500

У первого аккаунта баланс взялся из DEFAULT 0, потому что мы его не указали.

Что будет при нарушении

Если попытаться записать данные, нарушающие правило, база отклонит операцию с ошибкой. Например, отрицательный баланс не пройдёт CHECK:

CREATE TABLE accounts (
    id      INTEGER PRIMARY KEY,
    balance INTEGER CHECK (balance >= 0)
);

INSERT INTO accounts (balance) VALUES (-5);

Вывод:

Runtime error: CHECK constraint failed: balance >= 0

Аналогично попытка вставить дубликат в UNIQUE-столбец даст ошибку UNIQUE constraint failed, а NULL в NOT NULL-столбец — NOT NULL constraint failed. Ограничения — это страховка: лучше получить понятную ошибку при записи, чем испорченные данные потом.

Итог

  • INTEGER PRIMARY KEY — псевдоним rowid, эффективный автонумеруемый ключ.
  • AUTOINCREMENT запрещает переиспользование номеров; в большинстве случаев он не нужен.
  • NOT NULL, UNIQUE, DEFAULT, CHECK заставляют базу автоматически защищать данные.
Проверьте себя
1. Чем INTEGER PRIMARY KEY особенный в SQLite?
AОн хранит строки
BОн становится псевдонимом скрытого rowid и автонумеруется
CОн запрещает INSERT
DОн всегда требует AUTOINCREMENT
2. Что добавляет ключевое слово AUTOINCREMENT?
AУскоряет вставку
BЗапрещает повторное использование удалённых номеров id
CДелает столбец текстовым
DВключает внешние ключи
3. Что произойдёт при INSERT со значением, нарушающим CHECK (balance >= 0)?
AЗначение молча заменится на 0
BСтрока вставится, но станет невидимой
CБаза отклонит операцию с ошибкой CHECK constraint failed
DБаланс станет NULL
Поддержать проект