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

Представления (VIEW)

junior~25 мин

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

В прошлом уроке мы ускоряли запросы индексами. Теперь другая беда: один и тот же запрос приходится писать снова и снова. Каждое утро администратор кинотеатра собирает расписание из трёх таблиц: сеансы, фильмы, залы. Это пять строк с двумя JOIN, которые он помнит наизусть и всё равно иногда путает. Сегодня сохраним такой запрос в базе под именем и будем обращаться к нему как к таблице.

Сохранённый поиск

В приложении магазина можно сохранить поиск: «кроссовки 42 размера дешевле пяти тысяч». Магазин не откладывает для тебя кроссовки. Он помнит условия, и каждый раз, когда ты открываешь сохранённый поиск, заново ищет по сегодняшнему складу. Вчера подходящих пар было три, сегодня две продали и привезли пять новых, и сохранённый поиск покажет шесть. Переписывать условия не нужно, они уже сохранены.

Представление работает так же. Это запрос, который база хранит под именем. Своих строк у представления нет: при каждом обращении MySQL выполняет сохранённый запрос по текущим таблицам.

Первое представление

Сохраним расписание командой CREATE VIEW:

CREATE VIEW schedule AS
SELECT s.id, f.title, h.name AS hall, s.starts_at, s.price
FROM screenings s
JOIN films f ON f.id = s.film_id
JOIN halls h ON h.id = s.hall_id;
Query OK, 0 rows affected (0.00 sec)

Теперь schedule можно читать как обычную таблицу:

SELECT * FROM schedule ORDER BY starts_at;
+----+-------------------------------------------+----------------+---------------------+--------+
| id | title                                     | hall           | starts_at           | price  |
+----+-------------------------------------------+----------------+---------------------+--------+
|  1 | Брат                                      | Большой        | 2026-10-23 19:00:00 | 450.00 |
|  3 | Москва слезам не верит                    | Большой        | 2026-10-24 18:00:00 | 400.00 |
|  2 | Брат                                      | Малый          | 2026-10-24 21:30:00 | 350.00 |
|  5 | Сталкер                                   | Малый          | 2026-10-25 17:00:00 | 300.00 |
|  4 | Ирония судьбы                             | VIP            | 2026-10-25 20:00:00 | 900.00 |
|  6 | Ёжик в тумане                             | Большой        | 2026-10-26 11:00:00 | 150.00 |
|  7 | Новые имена                               | Малый          | 2026-10-26 22:00:00 | 250.00 |
+----+-------------------------------------------+----------------+---------------------+--------+
7 rows in set (0.00 sec)

Сортировку мы пишем в запросе к представлению, а не внутри него: так порядок виден там, где он нужен, и каждый отчёт сортирует по-своему.

С представлением работает весь SELECT из урока про выборку: WHERE, ORDER BY, LIMIT. Колонки называются так, как мы их назвали в запросе, поэтому зал здесь hall, а не h.name:

SELECT title, hall FROM schedule WHERE price < 400 ORDER BY title;
+--------------------------+----------------+
| title                    | hall           |
+--------------------------+----------------+
| Брат                     | Малый          |
| Ёжик в тумане            | Большой        |
| Новые имена              | Малый          |
| Сталкер                  | Малый          |
+--------------------------+----------------+
4 rows in set (0.00 sec)

«Ёжик» встал сразу за «Братом»: правило сравнения по умолчанию считает «ё» и «е» одной буквой.

Представление живёт рядом с таблицами. SHOW FULL TABLES показывает, где что:

SHOW FULL TABLES;
+----------------+------------+
| Tables_in_kino | Table_type |
+----------------+------------+
| films          | BASE TABLE |
| halls          | BASE TABLE |
| schedule       | VIEW       |
| screenings     | BASE TABLE |
| tickets        | BASE TABLE |
| viewers        | BASE TABLE |
+----------------+------------+
6 rows in set (0.00 sec)

BASE TABLE настоящая таблица со строками, VIEW сохранённый запрос. Таблица и представление делят одно пространство имён, поэтому назвать их одинаково нельзя.

Всегда свежие данные

Сделаем представление посложнее: продажи по фильмам, без возвращённых билетов. Группировку мы разбирали в уроке про GROUP BY.

CREATE VIEW sales_by_film AS
SELECT f.title, COUNT(t.id) AS tickets, SUM(s.price) AS revenue
FROM films f
JOIN screenings s ON s.film_id = f.id
JOIN tickets t ON t.screening_id = s.id
WHERE t.status <> 'refunded'
GROUP BY f.title;

SELECT * FROM sales_by_film ORDER BY revenue DESC;
+-------------------------------------------+---------+---------+
| title                                     | tickets | revenue |
+-------------------------------------------+---------+---------+
| Ирония судьбы                             |       2 | 1800.00 |
| Брат                                      |       4 | 1700.00 |
| Москва слезам не верит                    |       2 |  800.00 |
| Сталкер                                   |       1 |  300.00 |
| Ёжик в тумане                             |       1 |  150.00 |
+-------------------------------------------+---------+---------+
5 rows in set (0.00 sec)

Поднимем цену сеанса 3 и спросим ещё раз:

UPDATE screenings SET price = 500 WHERE id = 3;
SELECT * FROM sales_by_film ORDER BY revenue DESC;
+-------------------------------------------+---------+---------+
| title                                     | tickets | revenue |
+-------------------------------------------+---------+---------+
| Ирония судьбы                             |       2 | 1800.00 |
| Брат                                      |       4 | 1700.00 |
| Москва слезам не верит                    |       2 | 1000.00 |
| Сталкер                                   |       1 |  300.00 |
| Ёжик в тумане                             |       1 |  150.00 |
+-------------------------------------------+---------+---------+
5 rows in set (0.00 sec)

Выручка «Москвы» выросла с 800 до 1000, хотя представление мы не трогали. Оно не хранит цифры, а считает их заново при каждом SELECT.

Заодно этот пример ловит ошибку в нашей схеме. У билета нет своей цены, поэтому выручка считается по сегодняшней цене сеанса, и подорожание задним числом изменило старые продажи. Настоящая касса записывает цену в билет в момент продажи.

Раз представление считает заново, оно и работает не быстрее своего запроса. Индексы таблиц под ним работают как обычно, а своих индексов у представления нет. Готовых сохранённых результатов, как материализованные представления в PostgreSQL, в MySQL нет.

Как поменять представление

Второй CREATE VIEW с тем же именем не пройдёт:

CREATE VIEW schedule AS SELECT 1;
ERROR 1050 (42S01): Table 'schedule' already exists

Сервер называет представление таблицей: имена у них общие. Чтобы заменить запрос, пиши CREATE OR REPLACE VIEW. Уберём зал и добавим возрастной рейтинг:

CREATE OR REPLACE VIEW schedule AS
SELECT s.id, f.title, s.starts_at, s.price, f.age_rating
FROM screenings s JOIN films f ON f.id = s.film_id;

SELECT title, starts_at, age_rating FROM schedule WHERE age_rating = '12+' ORDER BY starts_at;
+-------------------------------------------+---------------------+------------+
| title                                     | starts_at           | age_rating |
+-------------------------------------------+---------------------+------------+
| Москва слезам не верит                    | 2026-10-24 18:00:00 | 12+        |
| Сталкер                                   | 2026-10-25 17:00:00 | 12+        |
+-------------------------------------------+---------------------+------------+
2 rows in set (0.00 sec)

Какой запрос сейчас сохранён, показывает SHOW CREATE VIEW. Ключ \G вместо точки с запятой печатает строку столбиком, иначе она не влезет в экран:

SHOW CREATE VIEW schedule\G
*************************** 1. row ***************************
                View: schedule
         Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `schedule` AS select `s`.`id` AS `id`,`f`.`title` AS `title`,`s`.`starts_at` AS `starts_at`,`s`.`price` AS `price`,`f`.`age_rating` AS `age_rating` from (`screenings` `s` join `films` `f` on((`f`.`id` = `s`.`film_id`)))
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

MySQL хранит не твой текст, а свою переписанную версию: каждая колонка с именем таблицы и псевдонимом. Отсюда ловушка: если создать представление через SELECT *, список колонок запишется на момент создания. Добавишь колонку в таблицу, и в представлении она не появится, пока не пересоздашь его. DEFINER и SQL SECURITY DEFINER значат, что запрос выполняется с правами создателя. Про права поговорим в уроке про пользователей.

ALGORITHM=UNDEFINED значит, что способ выполнения MySQL выбирает сам. Для простого представления он подставляет его текст прямо в твой запрос, словно ты написал JOIN руками, и условие WHERE из твоего запроса пользуется индексами таблиц. Для представления с GROUP BY такая подстановка невозможна: сервер считает результат во временную таблицу и лишь часть условий умеет протолкнуть внутрь. Тяжёлый запрос под представлением остаётся тяжёлым, даже если из результата тебе нужна одна строка.

Изменения через представление

Из простого представления над одной таблицей можно не только читать. Заведём список дешёвых сеансов:

CREATE VIEW cheap_screenings AS SELECT id, film_id, price FROM screenings WHERE price < 300;
SELECT * FROM cheap_screenings;
+----+---------+--------+
| id | film_id | price  |
+----+---------+--------+
|  6 |       5 | 150.00 |
|  7 |       6 | 250.00 |
+----+---------+--------+
2 rows in set (0.00 sec)
UPDATE cheap_screenings SET price = 200 WHERE id = 6;
SELECT id, price FROM screenings WHERE id = 6;
+----+--------+
| id | price  |
+----+--------+
|  6 | 200.00 |
+----+--------+
1 row in set (0.00 sec)

Такое представление называют обновляемым: каждая его строка соответствует ровно одной строке screenings, и MySQL знает, что менять.

А в sales_by_film строка «Брат» склеена из четырёх билетов. Какой из них менять, если написать tickets = 0? Сервер не угадывает и отказывает:

UPDATE sales_by_film SET tickets = 0 WHERE title = 'Брат';
ERROR 1288 (HY000): The target table sales_by_film of the UPDATE is not updatable

GROUP BY, агрегаты, DISTINCT и UNION делают представление необновляемым. На практике через представления редко пишут: удобнее менять таблицы напрямую, а представления держать для чтения.

Удаляем

DROP VIEW удаляет только сохранённый запрос, таблицы под ним остаются:

DROP VIEW cheap_screenings;
DROP VIEW cheap_screenings;
Query OK, 0 rows affected (0.00 sec)

ERROR 1051 (42S02): Unknown table 'kino.cheap_screenings'

Второй раз удалять нечего. В скриптах, которые запускают много раз, пиши DROP VIEW IF EXISTS: вместо ошибки будет предупреждение.

DROP VIEW IF EXISTS cheap_screenings;
Query OK, 0 rows affected, 1 warning (0.00 sec)

Зачем это всё

  • Спрятать сложность. Длинный JOIN пишут один раз, а отчёты и приложение читают короткое имя. Ошибку в условии исправляют в одном месте, а не в десяти отчётах.
  • Договор с читателями. Таблицы можно перестраивать, а представление с прежними колонками сохранит старые запросы рабочими. Разделил viewers на две таблицы, пересоздал представление с теми же колонками, и отчёт бухгалтерии ничего не заметил.
  • Ограничить доступ. Аналитику можно выдать право читать представление без телефонов и почты, не давая ему саму таблицу viewers.

Где представление лишнее: запрос нужен в одном месте программы, или он такой короткий, что имя ничего не объясняет. Каждое представление это ещё один объект, который надо помнить при изменении таблиц. Если удалить колонку, на которую ссылается представление, оно сломается, и узнаешь ты об этом только при следующем SELECT из него.

Что запомнить

  • Представление хранит не данные, а запрос, и каждый раз выполняет его заново.
  • Поэтому оно всегда показывает свежие данные из таблиц.
  • CREATE OR REPLACE VIEW меняет запрос, не трогая права и ссылки на имя.
  • Через простое представление над одной таблицей можно менять строки, через представление с группировкой нельзя.
  • SHOW FULL TABLES отличает представления от таблиц в колонке Table_type.

Попробуй сам

В песочнице база кинотеатра. Запрос создаёт расписание, читает его, меняет цену сеанса в таблице и показывает, что представление видит новую цену.

Добавь в представление возрастной рейтинг через CREATE OR REPLACE VIEW и выбери сеансы для зрителей до 12 лет. Создай представление над одной таблицей halls и попробуй сделать через него UPDATE. Потом попробуй то же с представлением, где есть GROUP BY.

Проверь себя

Задание

Дальше

Представление хранит один запрос. А если нужна целая последовательность действий с условиями, например «продать билет, если в зале есть места»? В следующем уроке разбираем хранимые процедуры, функции и триггеры.

домашка

Домашка