Весь прошлый кластер, до урока про подзапросы включительно, мы только читали данные. Но кинотеатр живёт: регистрируются зрители, появляются новые сеансы, растут цены, отменённые сеансы уходят из афиши. Сегодня три команды, которые меняют данные: INSERT добавляет строки, UPDATE меняет их, DELETE удаляет.
Журнал администратора
Представь журнал на столе администратора кинотеатра. Новый зритель: дописываешь строку в конец. Зритель переехал: зачёркиваешь старый город и вписываешь новый. Сеанс отменили: вычёркиваешь строку целиком. Перед тем как зачеркнуть, ты смотришь, что именно зачёркиваешь. У базы та же привычка полезна, и к концу урока станет ясно почему.
Все три команды отвечают одинаково: Query OK и число затронутых строк. Это число стоит читать каждый раз, оно первым показывает, что команда сделала не то.
INSERT: одна строка и много строк
В скобках после имени таблицы перечисляешь колонки, в VALUES значения в том же порядке:
INSERT INTO viewers (name, email, city, registered_at) VALUES ('Зоя Миронова', 'zoya@example.com', 'Тверь', '2026-10-24');
Query OK, 1 row affected (0.00 sec)
id мы не назвали: его выдаст AUTO_INCREMENT, как в уроке про числовые типы. phone тоже не назвали, и там будет NULL.
Несколько строк вставляют одной командой, перечислив их через запятую. Это быстрее, чем много отдельных INSERT: один поход к серверу вместо многих.
INSERT INTO viewers (name, email, registered_at) VALUES
('Игорь Лебедев', 'igor@example.com', CURDATE()),
('Кира Носова', 'kira@example.com', CURDATE());
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
У многострочной вставки появилась вторая строка ответа. Records: 2 значит «пришло две строки», Duplicates и Warnings пока нули, они понадобятся в следующем уроке. Проверим, что получилось:
SELECT id, name, city, registered_at FROM viewers WHERE id > 6;
+----+---------------------------+------------+---------------+
| id | name | city | registered_at |
+----+---------------------------+------------+---------------+
| 7 | Зоя Миронова | Тверь | 2026-10-24 |
| 8 | Игорь Лебедев | NULL | 2026-10-24 |
| 9 | Кира Носова | NULL | 2026-10-24 |
+----+---------------------------+------------+---------------+
3 rows in set (0.00 sec)
CURDATE() подставил сегодняшнюю дату. Часы песочницы стоят на 24 октября 2026 года.
Если пропустить обязательную колонку без значения по умолчанию, строгий режим не даст вставить строку:
INSERT INTO viewers (name, email) VALUES ('Мила', 'mila@example.com');
ERROR 1364 (HY000): Field 'registered_at' doesn't have a default value
У MySQL есть ещё запись INSERT ... SET, похожая на UPDATE. Колонка и значение стоят рядом, и в длинной вставке их труднее перепутать:
INSERT INTO viewers SET name = 'Лев Петров', email = 'lev@example.com', registered_at = '2026-10-24';
Query OK, 1 row affected (0.00 sec)
Это особенность MySQL, в PostgreSQL такой формы нет.
INSERT … SELECT: вставить ответ запроса
Вместо VALUES можно подставить SELECT, и в таблицу попадёт каждая строка его ответа. Поставим в VIP-зал вечерний показ для всех фильмов до 1976 года:
INSERT INTO screenings (film_id, hall_id, starts_at, price)
SELECT id, 3, '2026-10-27 19:00:00', 500 FROM films WHERE release_year < 1976;
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
SELECT id, film_id, hall_id, starts_at, price FROM screenings WHERE starts_at >= '2026-10-27';
+----+---------+---------+---------------------+--------+
| id | film_id | hall_id | starts_at | price |
+----+---------+---------+---------------------+--------+
| 8 | 3 | 3 | 2026-10-27 19:00:00 | 500.00 |
| 9 | 5 | 3 | 2026-10-27 19:00:00 | 500.00 |
| 10 | 7 | 3 | 2026-10-27 19:00:00 | 500.00 |
+----+---------+---------+---------------------+--------+
3 rows in set (0.00 sec)
Три фильма, три сеанса. Такую команду удобно сначала запустить как обычный SELECT и посмотреть, какие строки он вернёт.
UPDATE: Rows matched и Changed
UPDATE меняет колонки в строках, которые прошли WHERE. Поднимем цены в VIP-зале на десять процентов:
UPDATE screenings SET price = price * 1.1 WHERE hall_id = 3;
Query OK, 4 rows affected (0.00 sec)
Rows matched: 4 Changed: 4 Warnings: 0
SELECT id, hall_id, price FROM screenings WHERE hall_id = 3;
+----+---------+--------+
| id | hall_id | price |
+----+---------+--------+
| 4 | 3 | 990.00 |
| 8 | 3 | 550.00 |
| 9 | 3 | 550.00 |
| 10 | 3 | 550.00 |
+----+---------+--------+
4 rows in set (0.00 sec)
В ответе UPDATE два числа. Rows matched считает строки, которые подошли под WHERE. Changed считает строки, где значение и правда изменилось. Директор решил, что 990 рублей многовато, и выровнял все VIP-сеансы на 550:
UPDATE screenings SET price = 550.00 WHERE hall_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 4 Changed: 1 Warnings: 0
Подошли четыре сеанса, а изменился один, сеанс 4: у остальных цена уже была 550, и MySQL не стал их переписывать. Запусти ту же команду ещё раз:
UPDATE screenings SET price = 550.00 WHERE hall_id = 3;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 4 Changed: 0 Warnings: 0
0 rows affected, но Rows matched: 4: строки нашлись, менять было нечего. Совсем другое дело, когда не нашлось ничего:
UPDATE viewers SET city = 'Самара' WHERE id = 100;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
Ошибки нет. Зрителя 100 не существует, и UPDATE молча ничего не сделал. Если программа обновляет запись по номеру, она должна проверять Rows matched, иначе опечатка в номере пройдёт незамеченной.
В одном SET можно менять несколько колонок через запятую:
UPDATE viewers SET city = 'Москва', phone = '+79000000000' WHERE id = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Опечатка в имени колонки, наоборот, ловится сразу, ещё до того, как сервер тронет строки:
UPDATE films SET nosuch = 1;
ERROR 1054 (42S22): Unknown column 'nosuch' in 'field list'
Поэтому страшны не опечатки в именах, а ошибки в условии: запрос с неверным WHERE синтаксически правильный и выполнится без единого слова.
UPDATE с ORDER BY и LIMIT
В MySQL UPDATE по одной таблице принимает ORDER BY и LIMIT. Скидка 50 рублей на самый ранний сеанс:
UPDATE screenings SET price = price - 50 ORDER BY starts_at LIMIT 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
SELECT id, starts_at, price FROM screenings ORDER BY starts_at LIMIT 2;
+----+---------------------+--------+
| id | starts_at | price |
+----+---------------------+--------+
| 1 | 2026-10-23 19:00:00 | 400.00 |
| 3 | 2026-10-24 18:00:00 | 400.00 |
+----+---------------------+--------+
2 rows in set (0.00 sec)
Сеанс 1 подешевел с 450 до 400. Здесь работает правило из урока про SELECT: сортировка перед LIMIT должна быть без ничьих. Три новых VIP-сеанса начинаются в одно время, и ORDER BY starts_at DESC LIMIT 2 выбрал бы из них два каких-то. Какие, решит сервер. Для SELECT это неточный ответ, для UPDATE это изменённые не те строки. Добавляй в сортировку id.
Числа без знака не уходят в минус
У duration_min тип SMALLINT UNSIGNED. Попробуем вычесть из десятиминутного «Ёжика» двести минут:
UPDATE films SET duration_min = duration_min - 200 WHERE id = 5;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(`kino`.`films`.`duration_min` - 200)'
Если в выражении участвует беззнаковая колонка, результат тоже считается беззнаковым, а минус 190 в него не помещается. Строгий режим отказывает, и строка остаётся прежней.
DELETE: удаляем строки
DELETE удаляет строки, которые прошли WHERE. Уберём VIP-показы 27 октября и тестовых зрителей:
DELETE FROM screenings WHERE starts_at >= '2026-10-27';
Query OK, 3 rows affected (0.00 sec)
DELETE FROM viewers WHERE id > 6;
Query OK, 4 rows affected (0.00 sec)
Эти сеансы удалились легко, потому что билетов на них не было. Сеанс с билетами не даст себя удалить: на него ссылается внешний ключ из tickets, это мы разбирали в уроке про внешние ключи.
Полезная привычка: прежде чем удалять, запусти тот же WHERE в SELECT и посмотри на строки.
SELECT id, screening_id, seat, status FROM tickets WHERE status = 'refunded';
+----+--------------+------+----------+
| id | screening_id | seat | status |
+----+--------------+------+----------+
| 7 | 3 | D4 | refunded |
+----+--------------+------+----------+
1 row in set (0.00 sec)
Одна строка, та самая. Теперь удаляем и сверяем число:
DELETE FROM tickets WHERE status = 'refunded';
Query OK, 1 row affected (0.00 sec)
DELETE без WHERE
А теперь классическая авария. Человек хотел удалить один билет, начал набирать команду и нажал Enter раньше времени:
DELETE FROM tickets;
Query OK, 10 rows affected (0.00 sec)
SELECT COUNT(*) FROM tickets;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)
Ни предупреждения, ни вопроса «вы уверены?». DELETE без WHERE удаляет всё, UPDATE без WHERE меняет всё. Корзины нет, в обычном режиме клиента каждая команда сохраняется сразу.
Что помогает:
- сначала
SELECTс тем жеWHERE, потомDELETEилиUPDATE; - сверять число затронутых строк с ожидаемым;
- в клиенте
mysqlзапускать опцию--safe-updates: с нейUPDATEиDELETEбез условия по ключу и безLIMITне выполнятся; - опасные правки делать внутри транзакции, где ошибку можно отменить. Про них урок 19.
Что запомнить
INSERTс несколькими строками печатает строкуRecords, по ней видно, сколько строк вставлено.UPDATEсообщает, сколько строк подошло под условие (Rows matched) и сколько реально изменилось (Changed).ORDER BY ... LIMITвUPDATEиDELETEограничивает число затронутых строк, но порядок должен быть однозначным.- Колонка
UNSIGNEDне уходит в минус: вычитание даёт ошибку 1690. - Перед
DELETEвыполни тот жеWHEREвSELECTи посмотри, что уйдёт.
Попробуй сам
В песочнице база kino. Стартовый код добавляет зрителя, переносит его в другой город и удаляет, проверяя каждый шаг.
Запусти UPDATE второй раз и посмотри на Changed. Попробуй удалить сеанс 1, на который проданы билеты, и прочитай ошибку. Вставь трёх зрителей одной командой и найди строку Records.
Проверь себя
Задание
Дальше
Часто нужно «вставь, а если такая строка уже есть, обнови». Отдельным SELECT и INSERT это делать долго и опасно. В MySQL для этого есть одна команда: следующий урок про INSERT ... ON DUPLICATE KEY UPDATE.
домашка