Раздел 34 · MySQL с нуля

CREATE TABLE и первичный ключ

junior~25 мин

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

До сих пор мы жили в готовой базе 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, которого нет. В следующем уроке связываем таблицы внешними ключами, чтобы база сама следила за такими ссылками.

домашка

Домашка