Раздел 35 · SQLite с нуля

Внешние ключи и целостность

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В заказах «Пенки» есть столбец customer_id: номер гостя из таблицы customers. Так одна таблица ссылается на другую. В уроке про ограничения мы учили таблицу не принимать плохие строки, а в прошлом уроке меняли схему. Теперь научим базу следить, чтобы ссылки между таблицами вели туда, где кто-то есть.

Номерок в гардеробе

Ты сдаёшь куртку в гардероб и получаешь номерок 42. Номерок сам по себе ничего не значит: это ссылка на крючок с курткой. Хороший гардероб соблюдает два правила. Не выдаёт номерок на крючок, которого нет. И не снимает куртку с крючка, пока у кого-то на руках его номерок.

Столбец orders.customer_id и есть номерок. Правило «номер должен вести на существующего гостя» называют внешним ключом, а саму идею, что все ссылки в базе ведут на настоящие строки, ссылочной целостностью. Теорию разбирают урок про ключи и урок про NULL и целостность.

REFERENCES в схеме «Пенки»

Внешний ключ объявляют словом REFERENCES рядом со столбцом. В «Пенке» так описаны заказы:

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER REFERENCES customers(id),
  staff_id INTEGER NOT NULL REFERENCES staff(id),
  ordered_at TEXT NOT NULL,
  paid TEXT NOT NULL
);

Таблицу, на которую ссылаются, называют родительской, а ту, где лежит ссылка, дочерней. Здесь customers родитель, orders ребёнок. Проверим, бережёт ли база ссылки. Гостя с номером 99 в «Пенке» нет:

sqlite> PRAGMA foreign_keys;
╭──────────────╮
│ foreign_keys │
╞══════════════╡
│            0 │
╰──────────────╯
sqlite> INSERT INTO orders (customer_id, staff_id, ordered_at, paid) VALUES (99, 3, '2026-10-07 09:00:00', 'карта');
sqlite> SELECT id, customer_id, staff_id FROM orders WHERE customer_id = 99;
╭────┬─────────────┬──────────╮
│ id │ customer_id │ staff_id │
╞════╪═════════════╪══════════╡
│ 13 │          99 │        3 │
╰────┴─────────────┴──────────╯

Заказ на несуществующего гостя прошёл. Ответ на загадку в первой строке: PRAGMA foreign_keys равна 0. Проверка внешних ключей выключена.

PRAGMA foreign_keys = ON

Так сложилось исторически: внешние ключи появились в SQLite в 2009 году, и чтобы не сломать старые программы, их проверку оставили выключенной. Включать её нужно самому:

sqlite> PRAGMA foreign_keys = ON;
sqlite> PRAGMA foreign_keys;
╭──────────────╮
│ foreign_keys │
╞══════════════╡
│            1 │
╰──────────────╯
sqlite> INSERT INTO orders (customer_id, staff_id, ordered_at, paid) VALUES (98, 3, '2026-10-07 09:05:00', 'карта');
Error near line 3: FOREIGN KEY constraint failed
sqlite> DELETE FROM customers WHERE id = 1;
Error near line 4: FOREIGN KEY constraint failed

Теперь гардероб работает как надо. Номерок на гостя 98 не выдан. И Машу удалить нельзя: у неё три заказа, и они остались бы ссылками в никуда.

Главная ловушка: настройка живёт только в одном соединении, а в файл базы не записывается. Закрыл sqlite3, открыл снова, и проверка опять выключена:

sqlite> PRAGMA foreign_keys;
╭──────────────╮
│ foreign_keys │
╞══════════════╡
│            0 │
╰──────────────╯
sqlite> PRAGMA foreign_key_check(orders);
╭────────┬───────┬───────────┬──────╮
│ table  │ rowid │  parent   │ fkid │
╞════════╪═══════╪═══════════╪══════╡
│ orders │    13 │ customers │    1 │
╰────────┴───────┴───────────┴──────╯
sqlite> DELETE FROM orders WHERE id = 13;

Песочница ведёт себя так же: каждый запуск это новый sqlite3, и PRAGMA foreign_keys в нём снова 0. Поэтому PRAGMA foreign_keys = ON пишут первой строкой при каждом открытии базы, в коде программы тоже.

Вторая команда в этом протоколе, PRAGMA foreign_key_check, ищет строки, которые уже нарушают внешние ключи. Она нашла заказ 13 с гостем 99, который мы записали при выключенной проверке. Включение проверки старые ошибки не исправляет, их приходится находить и чинить руками, что мы и сделали через DELETE.

Что делать при удалении родителя

Запрет удалять Машу это поведение по умолчанию. Иногда нужно другое: удалить гостя вместе с его картой лояльности или удалить гостя, а подарок оставить ничьим. Для этого у REFERENCES есть действие ON DELETE. Заведём две таблицы:

sqlite> CREATE TABLE cards (
(x1...>   number TEXT PRIMARY KEY NOT NULL,
(x1...>   customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE
(x1...> );
sqlite> CREATE TABLE gifts (
(x1...>   id INTEGER PRIMARY KEY,
(x1...>   title TEXT NOT NULL,
(x1...>   customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL
(x1...> );
sqlite> INSERT INTO cards VALUES ('П-0005', 5), ('П-0006', 6);
sqlite> INSERT INTO gifts (title, customer_id) VALUES ('кружка', 6), ('пакет зёрен', 5);

NOT NULL у ключа карты стоит не просто так, помнишь странность из урока 10? Теперь удалим Кирилла, гостя номер 6, у которого нет заказов:

sqlite> PRAGMA foreign_keys = ON;
sqlite> DELETE FROM customers WHERE id = 6;
sqlite> SELECT * FROM cards;
╭────────┬─────────────╮
│ number │ customer_id │
╞════════╪═════════════╡
│ П-0005 │           5 │
╰────────┴─────────────╯
sqlite> SELECT * FROM gifts;
╭────┬─────────────┬─────────────╮
│ id │    title    │ customer_id │
╞════╪═════════════╪═════════════╡
│  1 │ кружка      │ NULL        │
│  2 │ пакет зёрен │           5 │
╰────┴─────────────┴─────────────╯

Карта Кирилла ушла вместе с ним: это CASCADE, каскадное удаление. Кружка осталась, но теперь ничья: SET NULL поставил в ссылку NULL. Без PRAGMA foreign_keys = ON не сработало бы ни то, ни другое. Те же действия есть в PostgreSQL, сравни с уроком про внешние ключи.

С CASCADE будь осторожен: одно удаление может потянуть за собой цепочку из сотен строк в нескольких таблицах. Для заказов оно обычно не подходит, историю продаж стирать нельзя.

Попробуй сам

Песочница работает на свежей базе «Пенки». Включи проверку и попробуй нарушить ссылки. Две команды нарочно падают:

Матча-латте никто не заказывал, поэтому её удаление прошло. Убери первую строку и запусти снова: без PRAGMA обе ошибки исчезнут, а foreign_key_check покажет испорченные ссылки.

Проверь себя

Задание

Дальше

Внешние ключи связали таблицы между собой. Какие бывают связи, один-ко-многим и многие-ко-многим, и как их рисовать таблицами, разбираем в следующем уроке.

домашка

Домашка