До сих пор мы жили в готовой базе kino и в прошлом уроке закончили разбирать типы колонок. Пора строить свои таблицы. Кинотеатр запускает осенние промокоды, и под них нужна новая таблица. Сегодня пишем CREATE TABLE, смотрим на результат двумя командами и разбираемся с главной колонкой любой таблицы, первичным ключом.
Номер паспорта
В городе может жить десять Анн Ковалёвых, и по имени их не различить. Поэтому у каждого человека есть номер паспорта: он один на всю страну, без него паспорт не выдадут, и по номеру всегда найдётся ровно один человек.
У строки таблицы такой номер называется первичным ключом. Два правила: значение не повторяется и не бывает пустым. Теория ключей подробно разобрана в уроке про ключи, здесь смотрим, как это выглядит в MySQL.
Первая таблица
Промокод сам по себе уникален: двух кодов OSEN26 быть не может. Значит, код и станет ключом:
CREATE TABLE promo_codes (
code VARCHAR(20) PRIMARY KEY,
discount TINYINT UNSIGNED NOT NULL,
valid_until DATE
);
Query OK, 0 rows affected (0.00 sec)
Внутри скобок перечислены колонки через запятую: имя, тип, необязательные уточнения. PRIMARY KEY объявляет ключ, NOT NULL запрещает пустое значение. Про NOT NULL и его соседей будет отдельный урок.
Что получилось, показывает DESCRIBE:
DESCRIBE promo_codes;
+-------------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+---------+-------+
| code | varchar(20) | NO | PRI | NULL | |
| discount | tinyint unsigned | NO | | NULL | |
| valid_until | date | YES | | NULL | |
+-------------+------------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
Колонки по строкам. Null отвечает, можно ли оставить значение пустым, Key показывает ключи: PRI значит первичный. Заметь, у code в графе Null стоит NO, хотя NOT NULL мы не писали. Первичный ключ сам запрещает пустоту.
Ключ в деле
Добавим промокод и попробуем добавить его ещё раз, маленькими буквами:
INSERT INTO promo_codes VALUES ('OSEN26', 15, '2026-11-30');
Query OK, 1 row affected (0.00 sec)
INSERT INTO promo_codes VALUES ('osen26', 10, NULL);
ERROR 1062 (23000): Duplicate entry 'osen26' for key 'promo_codes.PRIMARY'
Сюрприз: osen26 и OSEN26 для MySQL одно и то же. Правила сравнения строк по умолчанию не различают регистр, мы видели это в уроке про строки. Для промокодов это даже хорошо: покупатель, набравший код маленькими буквами, не получит второй, «другой» код. Но запомни: строковый ключ сравнивается по правилам сравнения, а не байт в байт.
Пустой ключ тоже не пройдёт:
INSERT INTO promo_codes VALUES (NULL, 10, NULL);
ERROR 1048 (23000): Column 'code' cannot be null
В таблице осталась одна строка:
SELECT * FROM promo_codes;
+--------+----------+-------------+
| code | discount | valid_until |
+--------+----------+-------------+
| OSEN26 | 15 | 2026-11-30 |
+--------+----------+-------------+
1 row in set (0.00 sec)
Как MySQL запомнил таблицу
DESCRIBE даёт сводку, а полный рецепт таблицы выдаёт SHOW CREATE TABLE. Вывод длинный, поэтому команду завершаем не точкой с запятой, а \G: клиент напечатает ответ столбиком вместо таблицы.
SHOW CREATE TABLE promo_codes\G
*************************** 1. row ***************************
Table: promo_codes
Create Table: CREATE TABLE `promo_codes` (
`code` varchar(20) NOT NULL,
`discount` tinyint unsigned NOT NULL,
`valid_until` date DEFAULT NULL,
PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
MySQL переписал наш запрос по-своему и дописал то, что мы не указали: NOT NULL у ключа, DEFAULT NULL у даты, движок InnoDB, кодировку и правила сравнения. Этот текст можно скопировать и создать такую же таблицу на другом сервере.
Как InnoDB хранит строки
Вспомни бумажный словарь. Слова в нём не лежат как попало: они стоят по алфавиту, и, чтобы найти слово, ты не листаешь книгу целиком, а открываешь её примерно посередине и сужаешь поиск.
InnoDB, движок из урока про движки хранения, хранит таблицу так же. Строки лежат в порядке первичного ключа, внутри дерева поиска по этому ключу. Такое устройство называют кластерным индексом: индекс по ключу и есть таблица. Поиск по первичному ключу поэтому самый быстрый, какой бывает. Если ключ не объявить, InnoDB возьмёт первую колонку с UNIQUE и NOT NULL, а если такой нет, заведёт скрытый номер строки, который ты не увидишь и не сможешь использовать. Поэтому ключ объявляй всегда. Для большинства таблиц лучший выбор короткий INT AUTO_INCREMENT, как id в таблицах kino.
Код или номер
У промокодов ключом стал сам код. Такой ключ называют естественным: он взят из жизни и что-то значит для людей. У зрителей в kino ключ другой, id с AUTO_INCREMENT, номер, который придумала база. Это суррогатный ключ.
Почему не взять ключом email зрителя? Email меняют, а ключ должен жить вечно: на него ссылаются билеты, и при смене ключа пришлось бы переписывать все ссылки. Кроме того, длинная строка в ключе раздувает каждый индекс таблицы, ведь InnoDB кладёт значение ключа в каждый из них. Естественный ключ хорош, когда значение короткое и не меняется, как код промоакции. В остальных случаях заводи id, а уникальность email обеспечь отдельным ограничением UNIQUE.
Ключ из двух колонок
Фильму можно дать несколько жанров. Заведём таблицу «фильм и жанр»: одна строка на пару. Ни film_id, ни genre по отдельности не уникальны, у «Брата» два жанра, драм много. Уникальна пара. Такой ключ называют составным, его пишут отдельной строкой после колонок:
CREATE TABLE film_genres (
film_id INT NOT NULL,
genre VARCHAR(30) NOT NULL,
PRIMARY KEY (film_id, genre)
);
INSERT INTO film_genres VALUES (1, 'драма'), (1, 'криминал'), (4, 'драма');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
Фильм 1 встречается дважды, «драма» тоже дважды, и всё прошло. А повтор пары не пройдёт:
INSERT INTO film_genres VALUES (1, 'драма');
ERROR 1062 (23000): Duplicate entry '1-драма' for key 'film_genres.PRIMARY'
В сообщении MySQL склеил значения обеих колонок через дефис. В DESCRIBE метка PRI стоит у обеих колонок:
DESCRIBE film_genres;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| film_id | int | NO | PRI | NULL | |
| genre | varchar(30) | NO | PRI | NULL | |
+---------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
Частые ошибки CREATE TABLE
Два первичных ключа в одной таблице:
CREATE TABLE bad_table (id INT PRIMARY KEY, code INT PRIMARY KEY);
ERROR 1068 (42000): Multiple primary key defined
Ключ один. Нужны две колонки, пиши составной ключ, как выше.
Счётчик без ключа:
CREATE TABLE bad_table (id INT AUTO_INCREMENT, name VARCHAR(10));
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key
AUTO_INCREMENT работает только на колонке-ключе. Обычно рядом пишут PRIMARY KEY.
Одно имя на две колонки и забытая длина VARCHAR:
CREATE TABLE bad_table (id INT, id VARCHAR(10));
ERROR 1060 (42S21): Duplicate column name 'id'
CREATE TABLE bad_table (name VARCHAR);
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1
Ошибка 1064 значит «не понял запрос». Слова near ')' показывают, где MySQL споткнулся: сразу после VARCHAR он ждал длину в скобках.
Таблица с таким именем уже есть:
CREATE TABLE promo_codes (code VARCHAR(20));
ERROR 1050 (42S01): Table 'promo_codes' already exists
В скриптах, которые запускают много раз, пишут IF NOT EXISTS. Тогда вместо ошибки будет пометка:
CREATE TABLE IF NOT EXISTS promo_codes (code VARCHAR(20));
Query OK, 0 rows affected, 1 warning (0.00 sec)
SHOW WARNINGS;
+-------+------+------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------+
| Note | 1050 | Table 'promo_codes' already exists |
+-------+------+------------------------------------+
1 row in set (0.00 sec)
Осторожно: IF NOT EXISTS не сравнивает определения. Старая таблица с тремя колонками осталась как была, новое описание с одной колонкой MySQL проигнорировал.
Удаляем таблицу
Создадим таблицу заметок, где дата ставится сама, и посмотрим список таблиц:
CREATE TABLE notes (id INT AUTO_INCREMENT PRIMARY KEY, body VARCHAR(200) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);
INSERT INTO notes (body) VALUES ('первая заметка');
SELECT * FROM notes;
+----+-----------------------------+---------------------+
| id | body | created_at |
+----+-----------------------------+---------------------+
| 1 | первая заметка | 2026-10-24 12:00:00 |
+----+-----------------------------+---------------------+
1 row in set (0.00 sec)
SHOW TABLES;
+----------------+
| Tables_in_kino |
+----------------+
| film_genres |
| films |
| halls |
| notes |
| promo_codes |
| screenings |
| tickets |
| viewers |
+----------------+
8 rows in set (0.00 sec)
DROP TABLE удаляет таблицу вместе с данными. Без вопросов и без корзины: на настоящем сервере перед ним стоит вспомнить про резервную копию.
DROP TABLE notes;
Query OK, 0 rows affected (0.00 sec)
Второй раз удалить не выйдет, таблицы уже нет. С IF EXISTS команда пройдёт с предупреждением:
DROP TABLE notes;
ERROR 1051 (42S02): Unknown table 'kino.notes'
DROP TABLE IF EXISTS notes;
Query OK, 0 rows affected, 1 warning (0.00 sec)
В PostgreSQL таблицы создаются почти так же, сравни с уроком про CREATE TABLE в PostgreSQL.
Что запомнить
CREATE TABLEзадаёт колонки и их типы,DESCRIBEиSHOW CREATE TABLEпоказывают, что сервер запомнил.- Первичный ключ уникален и не бывает
NULL, а повтор даёт ошибку 1062. - InnoDB хранит строки в порядке первичного ключа, поэтому короткий числовой ключ дешевле длинного текстового.
- Ключ из двух колонок запрещает повтор пары, но каждая колонка по отдельности может повторяться.
IF NOT EXISTSиIF EXISTSпревращают ошибку в предупреждение, но не проверяют, что таблица та же самая.
Попробуй сам
В песочнице создадим таблицу промокодов, посмотрим на неё и проверим, что ключ не пускает дубль в другом регистре:
Выполни SHOW CREATE TABLE promo_codes\G и найди строку с первичным ключом. Создай film_genres с составным ключом и вставь одну пару дважды. Попробуй объявить AUTO_INCREMENT без ключа и прочитай ошибку.
Проверь себя
Задание
Дальше
Колонка film_id в film_genres хранит номер фильма, но пока ничто не мешает записать туда номер 99, которого нет. В следующем уроке связываем таблицы внешними ключами, чтобы база сама следила за такими ссылками.
домашка