VIEW в SQL — что это и зачем нужны представления
Что такое VIEW
VIEW (представление) — это именованный SQL-запрос, сохранённый в базе данных. При обращении к VIEW база выполняет этот запрос и возвращает результат как обычную таблицу. Самих данных VIEW не хранит — это виртуальная таблица.
Проще говоря: вы пишете сложный SELECT один раз, даёте ему имя и дальше используете это имя вместо того, чтобы копировать запрос по всему коду.
CREATE VIEW — синтаксис
CREATE VIEW active_users AS
SELECT
user_id,
COUNT(*) AS sessions,
MAX(created_at) AS last_seen
FROM user_sessions
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY user_id;После создания active_users используется как таблица:
SELECT * FROM active_users WHERE sessions > 10;База данных подставляет сохранённый запрос каждый раз, когда вы обращаетесь к VIEW. Никакого кеширования, никакого предвычисления — просто алиас для SELECT.
Зачем нужны представления
1. Упрощение сложных запросов
Аналитик каждый день считает метрики из 4 таблиц с 3 JOIN и 2 подзапросами. Без VIEW этот запрос копируется в 10 дашбордов. С VIEW — вся логика инкапсулирована:
CREATE VIEW daily_metrics AS
SELECT
DATE_TRUNC('day', o.created_at) AS day,
COUNT(DISTINCT o.user_id) AS buyers,
COUNT(*) AS orders,
SUM(o.amount) AS revenue,
SUM(o.amount) / COUNT(DISTINCT o.user_id) AS arpu
FROM orders o
WHERE o.status = 'completed'
GROUP BY 1;Теперь SELECT * FROM daily_metrics WHERE day >= '2026-04-01' — и всё. Логика расчёта в одном месте, а не размазана по десяти запросам.
2. Ограничение доступа
VIEW позволяет дать пользователю доступ к части данных без доступа к исходной таблице:
CREATE VIEW public_users AS
SELECT user_id, username, city, created_at
FROM users;
-- GRANT SELECT ON public_users TO analyst_role;Аналитик видит public_users, но не видит email, phone, salary из таблицы users.
3. Единообразная логика
Если определение «активный пользователь» меняется — правите один VIEW, а не 20 запросов в разных скриптах.
Использование VIEW в запросах
VIEW работает везде, где работает таблица — в SELECT, JOIN, подзапросах:
-- JOIN с VIEW
SELECT u.username, m.orders, m.revenue
FROM users u
JOIN (SELECT * FROM daily_metrics) m ON ... -- не нужно, VIEW уже есть
-- Лучше: VIEW с сегментами пользователей
CREATE VIEW user_segments AS
SELECT
user_id,
CASE
WHEN total_spent > 10000 THEN 'vip'
WHEN total_spent > 1000 THEN 'regular'
ELSE 'new'
END AS segment
FROM (
SELECT user_id, COALESCE(SUM(amount), 0) AS total_spent
FROM orders
GROUP BY user_id
) t;
-- Использование
SELECT s.segment, COUNT(*) AS users
FROM user_segments s
GROUP BY s.segment;Обновление и удаление
-- Заменить определение VIEW
CREATE OR REPLACE VIEW active_users AS
SELECT user_id, COUNT(*) AS sessions
FROM user_sessions
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days' -- было 30, стало 7
GROUP BY user_id;
-- Удалить VIEW
DROP VIEW active_users;
-- Удалить, только если существует
DROP VIEW IF EXISTS active_users;CREATE OR REPLACE удобен: не нужно дропать и пересоздавать. Но он не позволяет менять список столбцов — только логику запроса.
MATERIALIZED VIEW
Обычный VIEW выполняет запрос при каждом обращении. Если запрос тяжёлый (агрегация миллионов строк), это медленно. Материализованное представление сохраняет результат на диск:
CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM orders
GROUP BY 1;Данные вычисляются один раз и хранятся физически. Запрос SELECT * FROM monthly_revenue читает готовые данные — моментально.
Но данные устаревают. Чтобы обновить:
REFRESH MATERIALIZED VIEW monthly_revenue;
-- С CONCURRENTLY — без блокировки чтения (нужен UNIQUE INDEX)
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_revenue;Когда использовать: тяжёлые аналитические агрегаты, которые не нужны в реальном времени. Дашборды, которые обновляются раз в час или раз в день. Отчёты по историческим данным.
VIEW vs CTE vs подзапрос
| VIEW | CTE (WITH) | Подзапрос | |
|---|---|---|---|
| Хранится в БД | Да | Нет — только в рамках запроса | Нет |
| Переиспользование | Между запросами | Внутри одного запроса | Нет |
| Доступ другим пользователям | Да | Нет | Нет |
| Материализация | На диск (REFRESH) | Внутри запроса (PG 12+ MATERIALIZED) | Нет |
| Когда использовать | Логика нужна в нескольких запросах/скриптах | Несколько шагов внутри одного запроса | Простая одноразовая фильтрация |
VIEW — для повторяющейся логики между запросами. CTE — для структурирования одного запроса. Подзапрос — для простых случаев. Подробнее о CTE и подзапросах — в отдельном разборе.
Типичные ошибки
1. VIEW поверх VIEW поверх VIEW
CREATE VIEW v1 AS SELECT ... FROM big_table;
CREATE VIEW v2 AS SELECT ... FROM v1 JOIN another_table ...;
CREATE VIEW v3 AS SELECT ... FROM v2 WHERE ...;Три уровня вложенности VIEW — и вы получаете запрос, который никто не может отладить. Оптимизатор разворачивает все VIEW в один запрос, и план выполнения становится непредсказуемым. Правило: один уровень вложенности VIEW нормально, два — максимум.
2. Забыть обновить MATERIALIZED VIEW
Создали материализованное представление, используете его в дашборде — а данные за вчера не появляются. Потому что REFRESH не запущен. Решение: настроить cron-задачу или pg_cron для автоматического обновления.
3. Обновляемый VIEW без понимания ограничений
В PostgreSQL можно делать INSERT/UPDATE/DELETE через простые VIEW (без JOIN, GROUP BY, DISTINCT). Но если VIEW содержит агрегацию или JOIN — запись невозможна. На собеседовании это частый вопрос.
Вопросы с собеседований
«Что такое VIEW и чем отличается от таблицы?» — VIEW не хранит данные. Это сохранённый запрос, который выполняется при каждом обращении. Таблица хранит данные физически на диске.
«Зачем нужен MATERIALIZED VIEW?» — Для кеширования результатов тяжёлых запросов. Данные вычисляются один раз и хранятся на диске. Обновляются по команде REFRESH. Подходит для дашбордов и аналитических отчётов.
«Можно ли делать INSERT в VIEW?» — Да, если VIEW простой: один SELECT из одной таблицы, без GROUP BY, DISTINCT, UNION, оконных функций. В остальных случаях — нет (или нужен INSTEAD OF триггер).
«VIEW замедляет запрос?» — Обычный VIEW — нет, это просто макрос. База разворачивает VIEW и оптимизирует итоговый запрос как единое целое. Проблемы возникают при глубокой вложенности VIEW в VIEW.
«Чем MATERIALIZED VIEW отличается от обычного VIEW?» — Обычный VIEW выполняет запрос каждый раз. Materialized VIEW хранит предвычисленный результат на диске. Быстрее при чтении, но данные могут устареть — нужен REFRESH.
FAQ
VIEW — это копия таблицы?
Нет. Обычный VIEW не хранит данных. Это сохранённый запрос, который выполняется при каждом обращении. Если данные в исходных таблицах изменились — VIEW автоматически покажет новые данные. Копия данных — это MATERIALIZED VIEW.
Как часто обновлять MATERIALIZED VIEW?
Зависит от задачи. Дашборд с дневными метриками — раз в сутки. Агрегат для поиска — раз в час. Критичные данные в реальном времени — не используйте MATERIALIZED VIEW, используйте обычный VIEW или прямой запрос. Типичный вариант: REFRESH MATERIALIZED VIEW CONCURRENTLY по cron-расписанию.
VIEW есть во всех СУБД?
Обычные VIEW поддерживают все основные СУБД: PostgreSQL, MySQL, ClickHouse, SQL Server, Oracle. MATERIALIZED VIEW есть в PostgreSQL и Oracle. В MySQL и SQL Server материализованных представлений нет нативно (в SQL Server есть indexed views — похожая концепция). В ClickHouse для этого используются движки таблиц (MaterializedView — это триггер, а не кеш).
VIEW тормозит запрос?
Сам по себе — нет. Обычный VIEW — это алиас для SELECT. Оптимизатор разворачивает его и строит план как для обычного запроса. Проблемы начинаются, если: (1) VIEW вложены друг в друга на 3+ уровня; (2) VIEW содержит тяжёлую логику, а вы используете его как таблицу в JOIN с другим тяжёлым VIEW. Решение — выносить тяжёлые части в MATERIALIZED VIEW.
Хотите потренировать VIEW и другие SQL-темы на реальных задачах с собеседований? Попробуйте SQL-тренажёр или откройте тренажёр прямо в Telegram. А если хотите увидеть, какие вопросы задают на собесах — загляните в примеры вопросов.