Learning Platform
Урок 12.05 · 16 мин
Средний
VIEWMATERIALIZED VIEWREFRESHUpdatable viewQuery abstraction

Зачем нужен VIEW

Вы написали красивый запрос на 30 строк с JOIN-ами, CASE, агрегатами. Через неделю коллега пишет такой же. Через месяц вы пишете похожий, но забыли одно условие, и цифры на дашборде разошлись с цифрами в выгрузке для финансов.

Решение — дать запросу имя. В SQL для этого есть

VIEW
. Это именованный SELECT, который ведёт себя как таблица: его можно 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 vs MATERIALIZED VIEW

Обычный view — это макро над запросом. Materialized view — это закешированный результат. У каждого свои tradeoffs.

VIEWименованный SELECT
хранитсятолько определение, не данные
скорость чтениякак у исходного запроса
свежестьвсегда актуальные данные
когда братьабстракция, безопасность, DRY
MATERIALIZED VIEWзакешированный результат
хранитсярезультат запроса как таблица
скорость чтениякак у обычной таблицы — быстро
свежестьна момент последнего REFRESH
когда братьдорогие агрегаты, отчёты, ETL

Создаём и используем VIEW

Простой view: «активные клиенты» = те, у кого есть заказы со статусом delivered

PostgreSQL

Внутри 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

PostgreSQL

Что произойдёт, если мы вставим новый заказ:

REFRESH: материализованный view не обновляется сам

PostgreSQL

Когда что использовать: practical guide

СценарийЧто брать
Скрыть сложный JOIN от прикладного кодаVIEW
Разграничить доступ (скрыть колонки)VIEW + GRANT
Простой фильтр над таблицей, нужны UPDATE через негоVIEW updatable + WITH CHECK OPTION
Дорогой агрегат для дашборда, OK данные «вчерашние»MATERIALIZED VIEW + cron REFRESH
Дорогой агрегат, нужны real-time данныеденормализация колонками + триггер
Очень сложная логика обновления через viewINSTEAD OF триггер

И главное: сначала просто индексируйте FK и измеряйте через EXPLAIN ANALYZE. View и materialized view — это не магия, а инструменты. Если запрос быстрый — не нужно ничего оборачивать.

View materialization в dbt — по умолчанию Table materialization в dbt — MATERIALIZED VIEW аналог Инкрементальные MV в ClickHouse: триггер на вставку

Чек-лист

  • 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.
Проверка знанийKnowledge check
Дашборд показывает «топ-10 категорий по выручке за последний месяц». Запрос — JOIN orders + order_items + products + categories с GROUP BY и SUM, выполняется 4 секунды. Дашборд открывают раз в минуту. Бизнес согласен на отставание в 5 минут. Что выбрать?
ОтветAnswer
Идеальный кандидат для MATERIALIZED VIEW. Создаём CREATE MATERIALIZED VIEW top_categories AS <тот же запрос>, добавляем уникальный индекс (например, по category_id), и настраиваем cron-job или pg_cron, который раз в 5 минут делает REFRESH MATERIALIZED VIEW CONCURRENTLY top_categories. Дашборд читает уже посчитанный снапшот за миллисекунды; основная схема не меняется; запись в orders/order_items остаётся обычной. Альтернатива «денормализация с триггером на каждой записи в order_items» — тяжелее в поддержке и не нужна, раз бизнес готов на 5-минутное отставание.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём принципиальная разница между CREATE VIEW и CREATE MATERIALIZED VIEW?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6