В прошлом уроке мы раскладывали по колонкам числа. Со строками в кинотеатре работы ещё больше: названия фильмов, имена зрителей, номера мест, статусы билетов. Здесь MySQL преподносит три сюрприза. Длина считается то в буквах, то в байтах. «Брат» и «БРАТ» для него одно и то же. А статус билета можно сравнить так, что «6+» окажется строже, чем «12+».
Бланк и блокнот
Представь два способа записать номер места. Первый: бланк с четырьмя клеточками. Пишешь «A1», а две оставшиеся клеточки всё равно занимают место на бумаге. Второй: строчка в блокноте. Сколько написал, столько и заняло, только в начале пометка, сколько букв в записи.
Бланк с клеточками это CHAR(4), строчка в блокноте VARCHAR(4). Число в скобках в обоих случаях задаёт, сколько символов влезет в колонку.
CHAR(N)дополняет короткое значение пробелами до N, а при чтении отрезает пробелы в конце.VARCHAR(N)хранит ровно то, что записали, плюс один-два байта с длиной.
Проверим, что станет с пробелами в конце:
CREATE TABLE seat_codes (code CHAR(4), label VARCHAR(4));
INSERT INTO seat_codes VALUES ('A1 ', 'A1 ');
SELECT CONCAT('[', code, ']'), CONCAT('[', label, ']'), LENGTH(code), LENGTH(label) FROM seat_codes;
+------------------------+-------------------------+--------------+---------------+
| CONCAT('[', code, ']') | CONCAT('[', label, ']') | LENGTH(code) | LENGTH(label) |
+------------------------+-------------------------+--------------+---------------+
| [A1] | [A1 ] | 2 | 4 |
+------------------------+-------------------------+--------------+---------------+
1 row in set (0.00 sec)
Мы обернули значения в квадратные скобки, чтобы пробелы стали видны. CHAR потерял два пробела, VARCHAR сохранил. Обычно это неважно, но если пробелы в конце что-то значат, CHAR их не сбережёт.
Когда брать CHAR? Почти никогда. Он хорош для значений, у которых длина всегда одна: код страны RU, код валюты RUB. Для всего остального бери VARCHAR.
Не влезло
Строка длиннее объявленной не запишется:
INSERT INTO seat_codes VALUES ('A12345', 'B1');
ERROR 1406 (22001): Data too long for column 'code' at row 1
Это снова строгий режим: без него сервер обрезал бы строку до A123 и промолчал. Длину выбирай с запасом, но в разумных пределах. Для имени хватит VARCHAR(50), для почты VARCHAR(100), как в таблице viewers.
Запас в длине ничего не стоит на диске: VARCHAR(255) и VARCHAR(50) хранят слово «Брат» одинаково, в четыре символа плюс байт длины. Число в скобках работает как проверка на входе, а не как размер ячейки. Поэтому выбирай его по смыслу: какое самое длинное имя или название ещё нормально, а какое уже явная ошибка.
Буквы и байты
Четыре клеточки это четыре символа, а не четыре байта. Запишем номер места по-русски:
INSERT INTO seat_codes VALUES ('Ряд7', 'Ряд7');
SELECT code, LENGTH(code), CHAR_LENGTH(code) FROM seat_codes;
+---------+--------------+-------------------+
| code | LENGTH(code) | CHAR_LENGTH(code) |
+---------+--------------+-------------------+
| A1 | 2 | 2 |
| Ряд7 | 7 | 4 |
+---------+--------------+-------------------+
2 rows in set (0.00 sec)
Ряд7 влез в CHAR(4): в нём четыре символа. Но LENGTH насчитал семь. Функция LENGTH считает байты, а CHAR_LENGTH символы. В кодировке utf8mb4, которую мы видели в уроке про базы, русская буква занимает два байта, а латинская и цифра один. Три русские буквы и цифра дают 3 × 2 + 1 = 7.
То же на названиях залов:
SELECT name, LENGTH(name), CHAR_LENGTH(name) FROM halls;
+----------------+--------------+-------------------+
| name | LENGTH(name) | CHAR_LENGTH(name) |
+----------------+--------------+-------------------+
| Большой | 14 | 7 |
| Малый | 10 | 5 |
| VIP | 3 | 3 |
+----------------+--------------+-------------------+
3 rows in set (0.00 sec)
Кстати, теперь понятно, почему колонка name в выводе такая широкая: клиент mysql отмеряет ширину в байтах, а «Большой» занимает 14 байтов. Когда проверяешь, влезет ли строка в колонку, считай через CHAR_LENGTH.
TEXT для длинного
VARCHAR годится для коротких строк. Для отзыва, описания фильма или текста статьи есть TEXT: длину указывать не нужно, влезет до 64 КБ.
CREATE TABLE reviews (id INT AUTO_INCREMENT PRIMARY KEY, film_id INT NOT NULL, body TEXT NOT NULL);
INSERT INTO reviews (film_id, body) VALUES (4, 'Медленно, но не отпускает до самого конца. Зона снится до сих пор.');
SELECT film_id, LEFT(body, 20), CHAR_LENGTH(body) FROM reviews;
+---------+--------------------------------------+-------------------+
| film_id | LEFT(body, 20) | CHAR_LENGTH(body) |
+---------+--------------------------------------+-------------------+
| 4 | Медленно, но не отпу | 66 |
+---------+--------------------------------------+-------------------+
1 row in set (0.00 sec)
LEFT(body, 20) вырезал первые 20 символов, удобно для превью в списке. Если текста больше 64 КБ, есть MEDIUMTEXT и LONGTEXT. Правило выбора простое: у значения есть разумный предел, и он меньше пары сотен символов, значит VARCHAR. Предела нет, значит TEXT.
«Брат» равно «БРАТ»
Поищем фильм заглавными буквами:
SELECT title FROM films WHERE title = 'БРАТ';
+----------+
| title |
+----------+
| Брат |
+----------+
1 row in set (0.00 sec)
Нашёлся. А «Ёжик» без буквы ё и со строчной:
SELECT title FROM films WHERE title = 'ежик в тумане';
+--------------------------+
| title |
+--------------------------+
| Ёжик в тумане |
+--------------------------+
1 row in set (0.00 sec)
Тоже нашёлся. Правила сравнения строк задаёт сопоставление. По умолчанию в MySQL 8.4 это utf8mb4_0900_ai_ci, и окончание имени расшифровывает его поведение: ai значит «без учёта надстрочных знаков» (accent insensitive), ci значит «без учёта регистра» (case insensitive). Посмотрим на четыре сравнения сразу (1 значит «да», 0 значит «нет»):
SELECT 'A' = 'a', 'ё' = 'е', 'й' = 'и', 'a' = 'a ';
+-----------+-------------+-------------+------------+
| 'A' = 'a' | 'ё' = 'е' | 'й' = 'и' | 'a' = 'a ' |
+-----------+-------------+-------------+------------+
| 1 | 1 | 0 | 0 |
+-----------+-------------+-------------+------------+
1 row in set (0.00 sec)
Регистр не важен, ё равна е. А вот й и и разные буквы, и пробел в конце строки тоже имеет значение.
Для поиска это удобно: зритель напишет «ежик», и фильм найдётся. Но то же правило действует и для UNIQUE. В таблице зрителей уже есть anna@example.com, и запись заглавными буквами сервер считает повтором:
INSERT INTO viewers (name, email, registered_at) VALUES ('Анна К.', 'ANNA@example.com', '2026-10-24');
ERROR 1062 (23000): Duplicate entry 'ANNA@example.com' for key 'viewers.email'
Для почты так и надо. А вот для кода купона или артикула, где регистр важен, такое сравнение опасно. В PostgreSQL всё наоборот: там 'Брат' = 'БРАТ' ложь, а без учёта регистра ищут через ILIKE, как в уроке про SELECT.
ENUM: список из меню
Последний тип для строк, у которых всего несколько возможных значений. Статус билета в учебной базе объявлен так: ENUM('paid', 'used', 'refunded'). ENUM похож на меню: выбрать можно только то, что в нём есть.
INSERT INTO tickets (screening_id, viewer_id, seat, status, sold_at) VALUES (7, 1, 'A1', 'lost', NOW());
ERROR 1265 (01000): Data truncated for column 'status' at row 1
Статуса lost в меню нет, и вставка не прошла. Сообщение Data truncated звучит странно, но значит именно это. А PAID заглавными пройдёт: сравнение без учёта регистра, и сервер запишет значение так, как оно названо в списке:
INSERT INTO tickets (screening_id, viewer_id, seat, status, sold_at) VALUES (7, 1, 'A1', 'PAID', NOW());
SELECT id, seat, status FROM tickets WHERE screening_id = 7;
+----+------+--------+
| id | seat | status |
+----+------+--------+
| 12 | A1 | paid |
+----+------+--------+
1 row in set (0.00 sec)
Внутри сервер хранит не текст, а номер пункта в списке: paid это 1, used 2. Отсюда первая ловушка: сортировка идёт по номеру, а не по алфавиту. Возрастной рейтинг фильмов тоже ENUM('0+', '6+', '12+', '16+', '18+'):
SELECT title, age_rating FROM films ORDER BY age_rating;
+-------------------------------------------+------------+
| title | age_rating |
+-------------------------------------------+------------+
| Ёжик в тумане | 0+ |
| Ирония судьбы | 6+ |
| Москва слезам не верит | 12+ |
| Сталкер | 12+ |
| Солярис | 12+ |
| Брат | 16+ |
| Новые имена | 18+ |
+-------------------------------------------+------------+
7 rows in set (0.00 sec)
Здесь это нам на руку: рейтинги идут от мягкого к строгому, как в списке. А вот вторая ловушка. Найдём фильмы строже «12+»:
SELECT title, age_rating FROM films WHERE age_rating > '12+';
+---------------------------+------------+
| title | age_rating |
+---------------------------+------------+
| Брат | 16+ |
| Ирония судьбы | 6+ |
| Новые имена | 18+ |
+---------------------------+------------+
3 rows in set (0.00 sec)
«Ирония судьбы» с рейтингом «6+» попала в строгие. При сравнении со строкой сервер сравнивает текст, а '6+' больше '12+', потому что символ 6 идёт после 1. Сортировка по номеру, сравнение по тексту: держи это в голове. Ещё одно неудобство: чтобы добавить в список новый статус, придётся менять таблицу через ALTER TABLE. Поэтому ENUM берут для коротких списков, которые почти не меняются.
Что запомнить
CHARхранит строку фиксированной длины и срезает пробелы в конце,VARCHARхранит ровно то, что дали.- Длина в скобках считается в символах, а
LENGTH()возвращает байты: русская буква занимает два. - Слишком длинная строка в строгом режиме даёт ошибку 1406, а не тихое обрезание.
- Сортировка
utf8mb4_0900_ai_ciне различает регистр и путает «ё» с «е», но «й» с «и» различает. ENUMпринимает только значения из списка и сортирует их по месту в списке, а не по алфавиту.
Попробуй сам
Вставь в seat_codes значение из пяти символов и прочитай ошибку. Найди зрителя по имени 'егор ёлкин' строчными буквами. Перепиши фильтр «строже 12+» так, чтобы он работал верно: перечисли нужные рейтинги через IN ('16+', '18+').
Проверь себя
Задание
Дальше
Числа и строки есть. В следующем уроке разберёмся с датами и временем: чем DATETIME отличается от TIMESTAMP, как на них влияет часовой пояс и что будет в 2038 году.
домашка