Зачем нужен VIEW
Вы написали красивый запрос на 30 строк с JOIN-ами, CASE, агрегатами. Через неделю коллега пишет такой же. Через месяц вы пишете похожий, но забыли одно условие, и цифры на дашборде разошлись с цифрами в выгрузке для финансов.
Решение — дать запросу имя. В SQL для этого есть
JOIN-ить, фильтровать, агрегировать.
CREATE VIEW active_customers AS
SELECT id, full_name, country
FROM customers
WHERE deleted_at IS NULL AND last_seen_at > now() - interval '30 days';
-- Теперь можно писать так:
SELECT country, COUNT(*) FROM active_customers GROUP BY country;
View — это не таблица, в нём нет данных. Он живёт как описание в системном каталоге. Каждый SELECT из view Postgres «разворачивает» в исходный запрос и оптимизирует целиком.
CREATE VIEW: чем хорош и чем нет
Плюсы:
- DRY: один источник истины для сложной логики. Изменили один view — изменилось везде, где он используется.
- Безопасность: можно дать пользователю права на view, не давая права на исходные таблицы. Классика — view, скрывающее
salary-колонку. - Абстракция: внешний код видит «таблицу клиентов», а под капотом —
JOINк CRM и какие-то фильтры. - Совместимость при миграциях: переименовали колонку — обновили view, внешний код не сломался.
Минусы:
- Производительность: view разворачивается на каждом запросе. Если в нём
JOINмиллионов строк — каждый раз эта работа повторяется. - Цепочки: view на view на view — Postgres всё равно развернёт это в один большой план, и понять, что происходит, становится трудно.
- Обновляемость: вставлять/обновлять данные через view можно, но с ограничениями.
Обычный view — это макро над запросом. Materialized view — это закешированный результат. У каждого свои tradeoffs.
Создаём и используем VIEW
Простой view: «активные клиенты» = те, у кого есть заказы со статусом delivered
Внутри view не сохраняется ничего, кроме определения. Каждый SELECT * FROM active_customers фактически исполняет тот SELECT ... FROM customers JOIN orders ....
Заменить определение можно через CREATE OR REPLACE VIEW, но с оговоркой: новая версия должна иметь те же колонки в том же порядке и тех же типах. Если хотите изменить структуру — сначала DROP VIEW, потом создавайте заново.
Updatable views: когда можно UPDATE через VIEW
По стандарту SQL UPDATE/INSERT/DELETE через view допустимы, если он «достаточно простой». В Postgres «достаточно простой» означает:
- Один источник в
FROM(безJOIN). - Нет
DISTINCT,GROUP BY,HAVING, агрегатов, window-функций. - Нет
UNION,INTERSECT,EXCEPT. - Нет
WITH(CTE).
Если view соответствует этим правилам — INSERT/UPDATE/DELETE будут работать «как ожидается»: Postgres транслирует операцию в эквивалентную на исходной таблице.
-- Updatable: один источник, простой WHERE
CREATE VIEW ru_customers AS
SELECT id, full_name, email FROM customers WHERE country = 'RU';
UPDATE ru_customers SET full_name = 'Аня С.' WHERE id = 1;
-- Эквивалентно: UPDATE customers SET full_name = 'Аня С.' WHERE id = 1 AND country = 'RU';
Хитрый момент: можно вставить через view строку, не попадающую под WHERE view. То есть в ru_customers можно вставить клиента с country = 'DE' — он успешно попадёт в customers, но не будет виден через сам view. Чтобы этого не было — WITH CHECK OPTION:
CREATE VIEW ru_customers AS
SELECT id, full_name, email, country FROM customers WHERE country = 'RU'
WITH CHECK OPTION;
-- Теперь INSERT с country='DE' через ru_customers упадёт.
Если view сложный, но «обновлять через него хочется» — можно навесить INSTEAD OF триггеры, которые вручную решают, что делать с INSERT/UPDATE/DELETE. Это уже за пределами введения; знать про возможность стоит.
CREATE MATERIALIZED VIEW
Materialized view создаётся почти тем же синтаксисом, но семантика принципиально другая: результат запроса сохраняется как таблица.
CREATE MATERIALIZED VIEW orders_by_country AS
SELECT c.country, COUNT(*) AS orders_count, SUM(p.amount_cents) AS revenue
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN payments p ON p.order_id = o.id
GROUP BY c.country;
Теперь SELECT * FROM orders_by_country читает уже посчитанную таблицу с агрегатами, а не пересчитывает JOIN + GROUP BY каждый раз. На больших данных разница — секунды vs миллисекунды.
Цена — данные не обновляются автоматически. Когда в orders приходят новые строки, в orders_by_country они не появятся, пока вы не сделаете:
REFRESH MATERIALIZED VIEW orders_by_country;
REFRESH пересчитывает view целиком и блокирует чтения на время операции. Если простой неприемлем, есть:
REFRESH MATERIALIZED VIEW CONCURRENTLY orders_by_country;
С CONCURRENTLY чтения не блокируются, но обновление работает дольше и требует уникального индекса на view. Создание индекса — отдельной командой:
CREATE UNIQUE INDEX ON orders_by_country (country);
Создаём MATERIALIZED VIEW и сравниваем с обычным VIEW
Что произойдёт, если мы вставим новый заказ:
REFRESH: материализованный view не обновляется сам
Когда что использовать: practical guide
| Сценарий | Что брать |
|---|---|
| Скрыть сложный JOIN от прикладного кода | VIEW |
| Разграничить доступ (скрыть колонки) | VIEW + GRANT |
| Простой фильтр над таблицей, нужны UPDATE через него | VIEW updatable + WITH CHECK OPTION |
| Дорогой агрегат для дашборда, OK данные «вчерашние» | MATERIALIZED VIEW + cron REFRESH |
| Дорогой агрегат, нужны real-time данные | денормализация колонками + триггер |
| Очень сложная логика обновления через view | INSTEAD OF триггер |
И главное: сначала просто индексируйте FK и измеряйте через EXPLAIN ANALYZE. View и materialized view — это не магия, а инструменты. Если запрос быстрый — не нужно ничего оборачивать.
Чек-лист
CREATE VIEW— это именованныйSELECT. Данные не хранятся; каждый запрос разворачивается в исходный.- View хорош для абстракции, безопасности (
GRANTна view без прав на таблицу), DRY-логики. - Updatable view в Postgres работает при одном источнике без
JOIN/DISTINCT/GROUP BY/агрегатов.WITH CHECK OPTIONзапрещает «вставку строк, невидимых через view». CREATE MATERIALIZED VIEWсохраняет результат как таблицу. Чтение быстрое, но данные «застывшие» доREFRESH MATERIALIZED VIEW.REFRESH ... CONCURRENTLYне блокирует чтения, но требует уникального индекса на view.- VIEW не обновляются автоматически — это не «реактивный» механизм. Логика обновления — задача приложения или cron.