Глиф Учебники

Урок 8. Ключи, связи и индексы

Курс «SQL» · урок 8 из 10 · 5 минут чтения

Ключи следят, чтобы данные не противоречили друг другу: не было двух товаров с одним номером и заказов от несуществующих покупателей. Индексы ускоряют поиск. Урок продолжает учебную базу: products, customers и orders из прошлых уроков.

Первичный ключ

Первичный ключ (PRIMARY KEY) — столбец, значение которого однозначно указывает на строку. Он уникален и не бывает пустым. У каждой таблицы курса это id: по номеру 3 всегда находится ровно один товар — рюкзак.

Ключом можно сделать и данные из жизни — номер паспорта, адрес почты, — но обычно заводят отдельный номер. Почту человек может сменить, а номер строки не меняется никогда, и на него спокойно ссылаются другие таблицы.

Ключ бывает и составным — из нескольких столбцов. Тогда уникальна комбинация значений:

SQL
CREATE TABLE order_items (
    order_id   INTEGER NOT NULL,
    product_id INTEGER NOT NULL,
    qty        INTEGER NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

Здесь в одном заказе товар может встретиться только один раз, но в разных заказах — сколько угодно.

Внешний ключ

Внешний ключ (FOREIGN KEY) — столбец, который ссылается на первичный ключ другой таблицы. В заказах это customer_id и product_id. Пока таблица orders создана без внешних ключей, в неё можно записать заказ от покупателя номер 99, которого нет. Пересоздайте её с проверкой:

SQL
PRAGMA foreign_keys = ON;

DROP TABLE orders;

CREATE TABLE orders (
    id          INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL REFERENCES customers (id),
    product_id  INTEGER NOT NULL REFERENCES products (id),
    qty         INTEGER NOT NULL CHECK (qty > 0),
    order_date  TEXT    NOT NULL
);

INSERT INTO orders (customer_id, product_id, qty, order_date) VALUES
    (1, 1, 10, '2026-09-01'),
    (1, 3, 1, '2026-09-01'),
    (2, 6, 1, '2026-09-03'),
    (3, 2, 20, '2026-09-05'),
    (3, 1, 5, '2026-09-10'),
    (4, 7, 1, '2026-09-12'),
    (2, 4, 2, '2026-09-15');

REFERENCES customers (id) значит: в customer_id может стоять только номер, который есть в customers.id. Та же запись в конце списка столбцов: FOREIGN KEY (customer_id) REFERENCES customers (id).

orders id PK customer_id FK product_id FK qty order_date customers id PK name city products id PK name … REFERENCES заказ: customer_id = 9 FOREIGN KEY constraint failed покупателя с id = 9 в customers нет
Внешние ключи customer_id и product_id в orders ссылаются на первичные ключи customers.id и products.id, и при включённой проверке заказ от несуществующего покупателя 9 не записывается.

В SQLite проверка внешних ключей по умолчанию выключена — ради совместимости со старыми базами. Её включает PRAGMA foreign_keys = ON;, и действует это только на текущее подключение: в каждом новом сеансе sqlite3 и в каждой программе команду нужно выполнять заново. В DB Browser for SQLite настройка Foreign Keys есть на вкладке «Edit Pragmas». В MySQL (таблицы InnoDB) и PostgreSQL внешние ключи проверяются всегда.

Теперь база не даст нарушить связи:

SQL
INSERT INTO orders (customer_id, product_id, qty, order_date)
VALUES (9, 1, 1, '2026-09-20');
Текст
FOREIGN KEY constraint failed

Покупателя номер 9 нет — заказ не записан. Удалить покупателя, у которого есть заказы, тоже нельзя: DELETE FROM customers WHERE id = 1; закончится той же ошибкой. PostgreSQL пишет подробнее: violates foreign key constraint "orders_customer_id_fkey" — с именем нарушенного ключа.

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

Что происходит со ссылками при удалении строки, на которую ссылаются, задаёт ON DELETE:

ПравилоЧто будет с заказами покупателя при его удалении
без правила (NO ACTION) или RESTRICTудалить покупателя нельзя, пока есть заказы
ON DELETE CASCADEзаказы удалятся вместе с покупателем
ON DELETE SET NULLв заказах customer_id станет NULL (столбец должен это допускать)
DELETE FROM customers WHERE id = 1; без правила или RESTRICT customers id name 1 Анна orders id customer_id 1 1 2 1 удалить нельзя, пока есть заказы ON DELETE CASCADE удалить вместе customers id name 1 Анна orders id customer_id 1 1 2 1 заказы удалятся вместе с покупателем ON DELETE SET NULL обнулить ссылку customers id name 1 Анна orders id customer_id 1 NULL 2 NULL заказы останутся, ссылка — NULL
При удалении покупателя 1 правило внешнего ключа решает судьбу его заказов: без правила удаление запрещено, CASCADE удаляет заказы вместе с ним, SET NULL оставляет заказы и ставит в customer_id NULL.
SQL
CREATE TABLE reviews (
    id          INTEGER PRIMARY KEY,
    product_id  INTEGER NOT NULL,
    body        TEXT    NOT NULL,
    FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE
);

ON DELETE CASCADE удаляет связанные строки молча. Один DELETE покупателя может стереть тысячи заказов в других таблицах. Ставьте его там, где зависимые строки без главной действительно не нужны (отзывы к товару), и не ставьте на заказы, платежи и всё, что нужно для учёта.

Виды связей

UNIQUE — уникальность без ключа

UNIQUE запрещает повтор значения в столбце, который не является первичным ключом: адрес почты, номер телефона, артикул. email TEXT UNIQUE — и вторая строка с тем же адресом не запишется: UNIQUE constraint failed.

Пустые значения (NULL) уникальность не нарушают: в SQLite, MySQL и PostgreSQL строк с NULL в таком столбце может быть сколько угодно.

Индексы

Без подсказок СУБД ищет строки перебором — читает таблицу целиком. На восьми товарах это незаметно, на миллионе строк — секунды на каждый запрос. Индекс — отдельная упорядоченная структура, как алфавитный указатель в конце книги: по значению столбца она сразу говорит, где лежат нужные строки.

SQL
CREATE INDEX idx_orders_customer ON orders (customer_id);

idx_orders_customer — имя индекса (любое, но лучше понятное), в скобках — столбцы. Запросы менять не нужно: СУБД сама решает, когда индекс полезен. Увидеть её решение помогает EXPLAIN QUERY PLAN в SQLite:

SQL
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 3;

До создания индекса в ответе SCAN orders — перебор всей таблицы. После:

Текст
SEARCH orders USING INDEX idx_orders_customer (customer_id=?)

SEARCH … USING INDEX — поиск по индексу. В MySQL и PostgreSQL то же показывает EXPLAIN SELECT ….

SELECT * FROM orders WHERE customer_id = 3; без индекса SCAN orders id customer_id 1 1 2 1 3 2 4 3 5 3 6 4 7 2 читаются все 7 строк с индексом SEARCH orders USING INDEX … customer_id id 1 1 1 2 2 3 2 7 3 4 3 5 4 6 индекс idx_orders_customer: значения по порядку и номера строк ищем 3 сразу строки 4 и 5 таблицы orders
Без индекса СУБД перебирает все строки orders (SCAN), а индекс idx_orders_customer хранит значения customer_id по порядку вместе с номерами строк, и поиск сразу находит строки 4 и 5 (SEARCH).

Какие индексы создаются сами

Для PRIMARY KEY и UNIQUE индекс создаётся автоматически. Для внешних ключей — по-разному: MySQL создаёт его сам, а SQLite и PostgreSQL — нет. Столбцы внешних ключей участвуют почти в каждом JOIN, поэтому индекс на них обычно стоит создать вручную.

Когда индекс нужен, а когда мешает

Индекс полезен на столбцах, которые часто стоят в WHERE, JOIN … ON и ORDER BY больших таблиц. Но у него есть цена: он занимает место, а каждый INSERT, UPDATE и DELETE должен обновить ещё и индекс. Индекс на каждый столбец «на всякий случай» замедлит запись и почти не ускорит чтение.

Удалить индекс: DROP INDEX idx_orders_customer; в SQLite и PostgreSQL, DROP INDEX idx_orders_customer ON orders; в MySQL. Данные при этом не пропадают. Уникальный индекс — CREATE UNIQUE INDEX: он и ускоряет поиск, и запрещает повторы.

Коротко

Проверьте себя

1. Почему в SQLite внешний ключ может не сработать, хотя REFERENCES в таблице есть?
2. Где ставится внешний ключ в связи «один покупатель — много заказов»?
3. Что даёт индекс на столбце customer_id?
4. Что сделает ON DELETE CASCADE у orders.customer_id?