VIEW в SQL: шпаргалка для аналитика

Зачем аналитику VIEW

VIEW — сохранённый SELECT, которым можно пользоваться как таблицей. В аналитике это основной инструмент для:

  • Обёртки сложных запросов в читабельные «таблицы».
  • Ограничения доступа (разные VIEW для разных ролей).
  • Унификации логики в команде — все считают retention одинаково.

Базовый синтаксис

CREATE VIEW active_users AS
SELECT user_id, name, last_login
FROM users
WHERE status = 'active';

-- Использование
SELECT * FROM active_users;
SELECT COUNT(*) FROM active_users WHERE last_login > CURRENT_DATE - INTERVAL '7 day';

View «прозрачна»: каждый раз, когда вы её запрашиваете, СУБД выполняет подлежащий запрос.

CREATE OR REPLACE VIEW

CREATE OR REPLACE VIEW active_users AS
SELECT user_id, name, last_login, email
FROM users WHERE status = 'active';

Для обновления структуры — удобнее, чем DROP + CREATE.

DROP VIEW

DROP VIEW IF EXISTS active_users;
-- Каскадно удалить зависимые view
DROP VIEW active_users CASCADE;

Updatable VIEW

Простые VIEW можно обновлять:

CREATE VIEW active_users AS SELECT * FROM users WHERE status = 'active';

UPDATE active_users SET last_login = NOW() WHERE user_id = 1;
-- Обновляет users, не active_users

Но только если в VIEW нет: JOIN, GROUP BY, DISTINCT, оконных функций. Иначе UPDATE не работает.

Если хочется сразу закрепить тему на практике — открой тренажёр в Telegram. 10 минут в день — и синтаксис в пальцах.

WITH CHECK OPTION

Запрещает вставку/обновление, которые нарушат условия VIEW:

CREATE VIEW active_users AS
SELECT * FROM users WHERE status = 'active'
WITH CHECK OPTION;

-- ❌ Будет ошибка — вставка status='inactive' нарушает условие
INSERT INTO active_users (name, status) VALUES ('X', 'inactive');

Materialized View

Обычная VIEW — это «запрос на лету». Materialized VIEW — сохранённый результат:

CREATE MATERIALIZED VIEW monthly_stats AS
SELECT DATE_TRUNC('month', created_at) AS month, SUM(amount) AS rev
FROM orders GROUP BY 1;

-- Использование как обычная таблица (быстро)
SELECT * FROM monthly_stats;

-- Обновить данные (принудительно)
REFRESH MATERIALIZED VIEW monthly_stats;

-- Concurrent refresh (без блокировки читателей)
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_stats;

Плюсы: быстрое чтение (данные физически хранятся).

Минусы: данные не свежие — требуется REFRESH. Обычно делают по расписанию (cron, pg_cron, Airflow).

VIEW vs CTE vs Temp Table

Критерий VIEW CTE Temp table
Область видимости БД (постоянно) один запрос сессия
Переиспользование ✅ во всех запросах только в текущем только в сессии
Производительность выполняется каждый раз inline обычно может иметь индексы
Сохраняет данные ❌ (обычная VIEW)

Вложенные VIEW

CREATE VIEW customer_stats AS
SELECT user_id, COUNT(*) AS orders, SUM(amount) AS revenue
FROM orders GROUP BY user_id;

CREATE VIEW top_customers AS
SELECT * FROM customer_stats WHERE revenue > 10000;

Можно — но осторожно. Вложения 3+ уровней делают запросы медленными и сложными в отладке.

Типовые применения

1. Унификация расчёта метрик

CREATE VIEW retention_daily AS
SELECT
  cohort_date,
  COUNT(*) AS cohort_size,
  COUNT(*) FILTER (WHERE days_since_signup = 7) * 1.0 / COUNT(*) AS d7
FROM user_activity GROUP BY cohort_date;

Все в команде считают retention одинаково, через один VIEW.

2. Ограничение доступа

CREATE VIEW public_users AS
SELECT id, name, city FROM users;  -- без email, phone

GRANT SELECT ON public_users TO role_external;

3. Упрощение сложных запросов

CREATE VIEW customer_stats AS
SELECT u.user_id, u.name, u.city,
       COUNT(o.order_id) AS orders,
       SUM(o.amount) AS revenue
FROM users u LEFT JOIN orders o USING (user_id)
GROUP BY u.user_id, u.name, u.city;

Теперь команда вместо 10-строчного JOIN пишет SELECT * FROM customer_stats.

Чтобы не только читать теорию, но и решать реальные задачи — загляните в бот Карьерника. Там по каждой теме подборка вопросов с разборами.

Когда VIEW — плохая идея

1. Очень сложная логика с join 5+ таблиц

Оптимизатор может не справиться, VIEW будет тормозить. Вынести в CREATE TABLE AS SELECT (snapshot) или dbt-модель.

2. Часто меняющаяся структура

Переименовать столбец — ломается VIEW, ломается weekly-отчёт, ломается кто-то из команды. Документируйте дependencies.

3. Для критичной производительности

Обычная VIEW — каждый запрос заново. Если нужно быстро — materialized view с расписанным REFRESH.

10 практических задач

1. VIEW активных пользователей

CREATE VIEW active_users AS
SELECT * FROM users WHERE status = 'active';

2. VIEW суммарной статистики по клиенту

CREATE VIEW customer_summary AS
SELECT user_id, COUNT(order_id) AS orders, SUM(amount) AS revenue,
       MIN(created_at) AS first_order, MAX(created_at) AS last_order
FROM orders GROUP BY user_id;

3. VIEW скрывающая персональные данные

CREATE VIEW users_public AS
SELECT id, name, city, created_at FROM users;

4. MATERIALIZED VIEW для дневной статистики

CREATE MATERIALIZED VIEW daily_gmv AS
SELECT created_at::DATE AS day, SUM(amount) AS gmv
FROM orders GROUP BY 1;

-- Обновлять каждую ночь через cron
REFRESH MATERIALIZED VIEW daily_gmv;

5. Обновление View после изменения структуры

CREATE OR REPLACE VIEW customer_summary AS
SELECT user_id, COUNT(order_id) AS orders,
       SUM(amount) AS revenue,
       AVG(amount) AS aov  -- новое
FROM orders GROUP BY user_id;

6. VIEW с параметрами через FUNCTION (PostgreSQL)

CREATE FUNCTION get_user_orders(uid INT)
RETURNS TABLE(order_id INT, amount DECIMAL) AS $$
  SELECT order_id, amount FROM orders WHERE user_id = uid;
$$ LANGUAGE SQL;

SELECT * FROM get_user_orders(1);

7. VIEW для retention

CREATE VIEW user_retention AS
SELECT user_id,
  (MIN(event_time)::DATE) AS signup_date,
  (MAX(event_time)::DATE - MIN(event_time)::DATE) AS days_active
FROM events GROUP BY user_id;

8. Зависимости VIEW

-- Найти, какие VIEW зависят от таблицы users
SELECT viewname FROM pg_views WHERE definition LIKE '%users%';

9. Дроп с каскадом

DROP VIEW customer_summary CASCADE;
-- Удалит все зависимые VIEW

10. VIEW как «секьюрити слой»

-- Менеджер видит только заказы своего региона
CREATE VIEW regional_orders AS
SELECT * FROM orders
WHERE region = current_setting('app.current_region');

Как тренироваться

VIEW — не учится отдельно. Используйте в реальных проектах: когда замечаете, что один и тот же JOIN в разных местах — вынесите в VIEW.

Совет: на собесе, если показывают сложный запрос и спрашивают «как улучшить» — предложите VIEW. Это чистота + переиспользование.

Читайте также

FAQ

VIEW медленная — что делать?

Варианты: (1) добавить индексы на базовые таблицы, (2) переписать в materialized view с REFRESH, (3) разбить на несколько view.

MATERIALIZED VIEW vs таблица со снапшотом?

Materialized view автоматически пересчитывается при REFRESH. Таблица — вы сами управляете. Для ежечасных метрик — materialized. Для редких отчётов — таблица через CREATE TABLE AS SELECT.

Можно ли INSERT в VIEW?

Можно в updatable view (простые VIEW без JOIN/GROUP BY). Изменения попадают в базовую таблицу. На практике редко используют.

Разница VIEW и stored procedure?

VIEW — сохранённый SELECT, возвращает «таблицу». Procedure — блок кода, может содержать логику, циклы, DML. В аналитике чаще VIEW.