PostgreSQL

Views и Materialized Views

Аналитический запрос на Shopify занимает 45 секунд - JOIN пяти таблиц по 100M строк. После materialized view с индексом - 12 миллисекунд. Те же данные, тот же результат. Разница в 3750 раз. Views и matviews - это архитектурный слой между схемой БД и приложением: прячут сложность, ускоряют чтение, дают безопасный доступ без раскрытия структуры таблиц.

  • **GitLab** использует views как абстракцию над партиционированными таблицами: когда схема партиций меняется, тысячи запросов приложения не требуют правок
  • **Confluence (Atlassian)** - matview для счётчиков просмотров страниц: агрегат по 500M строк пересчитывается раз в 5 минут, запросы пользователей всегда попадают в кеш
  • **Shopify** - security_barrier views для multi-tenancy: каждый магазин видит только свои данные через одну таблицу, фильтр по shop_id встроен в view и не может быть обойдён оптимизатором

Views Basics

**View - это сохранённый SQL-запрос, который ведёт себя как таблица.** Никаких данных на диске - только определение запроса. При каждом обращении к view PostgreSQL подставляет исходный SELECT и выполняет его. Это не кеш, не копия, не снимок - это именованный запрос.

**Views не хранят данные.** Планировщик PostgreSQL видит view как подзапрос и оптимизирует его вместе с внешним запросом. Это значит, что индексы на базовых таблицах работают через views - фильтры проталкиваются внутрь.

Gitlab использует views как слой абстракции над партиционированными таблицами - тысячи запросов приложения не меняются при перестройке схемы партиций. Shopify прячет multi-tenancy за views: одна таблица `orders`, view `shop_orders` добавляет `WHERE shop_id = current_setting('app.shop_id')::bigint`.

**CREATE OR REPLACE VIEW** может только добавлять столбцы в конец списка - нельзя менять типы или порядок существующих. Для радикального изменения нужно DROP + CREATE, что ломает зависимые objects (другие views, functions). Планируйте схему view заранее.

View `revenue_by_country` использует таблицу `orders` с индексом на `country`. Запрос `SELECT * FROM revenue_by_country WHERE country = 'US'` использует этот индекс?

Materialized Views

**Materialized view (matview) - это view, результат которого сохранён на диске.** В отличие от обычного view, matview хранит реальные данные: строки, страницы, можно создавать индексы. Запрос к matview не обращается к базовым таблицам - читает сохранённую копию. Цена: данные устаревают до следующего REFRESH.

ХарактеристикаОбычная ViewMaterialized View
Данные на дискеНетДа
Скорость чтенияЗависит от базовых таблицОчень быстро (нет JOIN)
АктуальностьВсегда свежиеДо следующего REFRESH
ИндексыНельзяМожно
Потребление диска0Полный размер результата

**Когда matview выгоден:** агрегаты по миллионам строк (дашборды), сложные JOIN которые пересчитываются редко, аналитические отчёты за прошлые периоды. Confluence (Atlassian) использует matview для счётчиков страниц - запрос падал с 3 сек до 2 мс.

Matview `daily_stats` обновляется раз в час через cron. Пользователь делает транзакцию в 10:59. В 11:00 запускается REFRESH. Что увидит следующий запрос к matview в 11:01?

Refresh Concurrently

**Обычный REFRESH MATERIALIZED VIEW берёт эксклюзивную блокировку** - никто не может читать matview пока идёт обновление. Для дашбордов с обновлением каждые 5 минут это неприемлемо. `REFRESH CONCURRENTLY` решает проблему: обновление идёт в фоне, пользователи читают старые данные без блокировки.

**REFRESH CONCURRENTLY требует уникальный индекс** - без него ошибка. Работает медленнее обычного REFRESH (вычисляет diff). Не работает если matview создана с `WITH NO DATA`. Первый populate должен быть обычным REFRESH.

Почему REFRESH MATERIALIZED VIEW CONCURRENTLY требует уникальный индекс?

Updatable Views

**PostgreSQL автоматически делает view обновляемой** если она соответствует условиям: один источник, нет DISTINCT/GROUP BY/HAVING/LIMIT/UNION, нет агрегатных функций. Через такую view можно делать INSERT/UPDATE/DELETE - они транслируются в операции над базовой таблицей.

**WITH CHECK OPTION** гарантирует, что INSERT/UPDATE через view создаёт только строки, видимые в этой view. Попытка вставить `is_active = FALSE` через `active_products` вызовет ошибку `new row violates check option`.

View `eu_customers` содержит `WHERE country IN ('DE', 'FR', 'ES')`. С `WITH CHECK OPTION`, что произойдёт при `INSERT INTO eu_customers (name, country) VALUES ('John', 'US')`?

Security Barrier

**Security barrier view** закрывает уязвимость: без неё функции в WHERE внешнего запроса могут выполниться ДО фильтра view, утекая данные через side effects. Например, `SELECT * FROM user_data WHERE log_access(id)` при оптимизации может вызвать `log_access` для строк, которые view должна скрыть.

**security_barrier снижает производительность** - планировщик не может проталкивать условия внутрь view для использования индексов. Используйте только там, где view реально служит защитным слоем (Row-Level Security обычно лучше для этой цели).

**PostgreSQL 9.2+** поддерживает `security_barrier`. В большинстве случаев Row-Level Security (RLS) - более гибкая альтернатива: политики на уровне таблицы, работают для всех запросов, не только через view.

Materialized view автоматически обновляется при изменении базовых таблиц

Matview обновляется только вручную через REFRESH MATERIALIZED VIEW или по расписанию через pg_cron/cron jobs

PostgreSQL не отслеживает зависимости matview от базовых таблиц для автообновления. Это архитектурное решение - автоматический REFRESH при каждом INSERT/UPDATE сводит на нет всю пользу matview. Актуальность данных - ответственность разработчика

Компания создала view `client_orders` с `WHERE client_id = get_current_client()`. Атакующий выполняет `SELECT * FROM client_orders WHERE spy_function(total)`. Что предотвращает security_barrier?

Итоги

  • **View = именованный запрос** без данных на диске. Планировщик разворачивает в подзапрос, индексы базовых таблиц работают
  • **Materialized view = снимок результата** на диске. Можно индексировать, скорость чтения постоянна, данные устаревают до REFRESH
  • **REFRESH CONCURRENTLY** обновляет без блокировки чтения, но требует уникальный индекс и работает медленнее
  • **WITH CHECK OPTION** на updatable view гарантирует, что INSERT/UPDATE не создаст строки вне фильтра view
  • **security_barrier** фиксирует порядок предикатов: фильтр view выполняется первым, пользовательские функции не видят скрытые строки

Связанные темы

Views и matviews тесно связаны с несколькими другими механизмами PostgreSQL:

  • Партиционирование — Views используются как абстракция над партиционированными таблицами - приложение видит одну view, PostgreSQL маршрутизирует к нужным партициям
  • Безопасность и права — Security_barrier views и Row-Level Security решают похожую задачу изоляции данных - RLS обычно предпочтительнее для production
  • Мониторинг и pg_stat — Системные catalog views (pg_stat_user_tables, pg_matviews) - сами являются views над системными таблицами

Вопросы для размышления

  • Дашборд компании загружает данные за последние 90 дней из таблицы с 500M строк. Каждый запрос занимает 8 секунд. Как materialized view с REFRESH CONCURRENTLY каждые 15 минут изменит ситуацию? Какие компромиссы это создаёт?
  • Приложение использует обычную view `customer_view` с фильтром по tenant_id. Разработчик добавляет WHERE с пользовательской функцией `log_query(id)`. При каком условии это становится уязвимостью безопасности?
  • Почему REFRESH MATERIALIZED VIEW CONCURRENTLY требует уникальный индекс, а обычный REFRESH - нет? Что происходит на уровне строк при каждом из этих подходов?

Связанные уроки

  • db-06-sql-advanced
Views и Materialized Views

0

1

Войти