SQLLab
Все статьи

VIEW в SQL: представления, материализованные вью и когда их использовать

Что такое VIEW в PostgreSQL, как создавать, обновлять, удалять. Отличие от материализованного VIEW, обновляемые вью, безопасность через RLS.

16 марта 2026 г.·5 мин чтения·

VIEW (представление) — это именованный SQL-запрос, сохранённый в базе данных. Обращаться к нему можно как к таблице. Разберём что это даёт, когда использовать и когда избегать.

Что такое VIEW

-- Создать VIEW
CREATE VIEW active_users AS
SELECT id, email, name, created_at, streak_days
FROM users
WHERE is_active = true AND deleted_at IS NULL;

-- Использовать как таблицу
SELECT * FROM active_users WHERE streak_days > 7;
SELECT COUNT(*) FROM active_users;

VIEW не хранит данные — каждый раз при обращении выполняется исходный запрос.


Зачем нужны VIEW

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

-- Без VIEW — повторяющийся сложный запрос
SELECT u.name, u.email, COUNT(o.id) AS order_count, SUM(o.amount) AS total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'completed'
GROUP BY u.id, u.name, u.email;

-- С VIEW — один раз создать, многократно использовать
CREATE VIEW user_stats AS
SELECT
    u.id,
    u.name,
    u.email,
    COUNT(o.id)    AS order_count,
    SUM(o.amount)  AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'completed'
GROUP BY u.id, u.name, u.email;

-- Использование
SELECT * FROM user_stats WHERE total_spent > 10000;

2. Безопасность — скрыть данные

-- Скрыть чувствительные поля
CREATE VIEW public_users AS
SELECT id, name, avatar_url, bio, created_at
FROM users
-- Не включаем: email, phone, password_hash, payment_data
WHERE is_active = true;

-- Дать доступ только к VIEW
GRANT SELECT ON public_users TO readonly_role;
REVOKE SELECT ON users FROM readonly_role;

3. Абстракция от схемы

-- Если переименовали таблицу — VIEW остался совместимым
CREATE VIEW orders AS SELECT * FROM purchase_orders;
-- Старый код продолжает работать через VIEW

CREATE OR REPLACE VIEW

-- Обновить VIEW без DROP + CREATE
CREATE OR REPLACE VIEW active_users AS
SELECT id, email, name, created_at, streak_days, elo_rating
FROM users
WHERE is_active = true;

-- Ограничение: нельзя изменить порядок или тип столбцов
-- В таком случае нужно DROP + CREATE

DROP VIEW

DROP VIEW active_users;
DROP VIEW IF EXISTS active_users;
DROP VIEW active_users CASCADE; -- вместе с зависимыми объектами

Обновляемые VIEW

Простые VIEW (без JOIN, агрегации, DISTINCT) поддерживают INSERT/UPDATE/DELETE:

CREATE VIEW my_tasks AS
SELECT id, title, status, due_date
FROM tasks
WHERE assigned_to = current_user;

-- Работает если VIEW обновляемый:
UPDATE my_tasks SET status = 'done' WHERE id = 5;
INSERT INTO my_tasks (title, due_date) VALUES ('Новая задача', '2026-04-01');

Условия для обновляемости:

  • Один источник (один FROM без JOIN)
  • Нет DISTINCT, GROUP BY, HAVING, LIMIT, OFFSET
  • Нет агрегатных и оконных функций
  • Нет UNION / EXCEPT / INTERSECT

WITH CHECK OPTION

Запрещает INSERT/UPDATE, нарушающие условие WHERE:

CREATE VIEW my_tasks AS
SELECT * FROM tasks WHERE assigned_to = current_user
WITH CHECK OPTION;

-- Ошибка: assigned_to не current_user
INSERT INTO my_tasks (title, assigned_to) VALUES ('Чужая задача', 99);

Материализованный VIEW

Обычный VIEW выполняет запрос при каждом обращении. Материализованный — хранит результат.

-- Создать материализованный VIEW
CREATE MATERIALIZED VIEW monthly_stats AS
SELECT
    DATE_TRUNC('month', created_at) AS month,
    COUNT(*) AS orders_count,
    SUM(amount) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1
ORDER BY 1;

-- Обновить данные
REFRESH MATERIALIZED VIEW monthly_stats;

-- Обновить без блокировки чтения (но нужен UNIQUE индекс)
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_stats;

-- Индексы на матвью работают!
CREATE INDEX ON monthly_stats (month);

Когда использовать материализованный VIEW:

  • Тяжёлый запрос (COUNT миллионов строк, сложные JOIN)
  • Данные не должны быть свежими в реальном времени (обновляется раз в час/день)
  • Нужны индексы на результате

Когда не использовать:

  • Данные должны быть актуальны сразу после изменений
  • Запрос быстрый — нет смысла платить за хранение

Производительность VIEW

VIEW не кешируют — каждый запрос пересчитывается:

-- Плохо: сложный VIEW поверх другого сложного VIEW
CREATE VIEW stats AS SELECT * FROM heavy_query_view WHERE ...;

PostgreSQL применяет оптимизацию «view merging» — встраивает запрос VIEW в основной. Но с несколькими уровнями вложенности оптимизатор может запутаться.

Проверяйте EXPLAIN ANALYZE при работе со сложными VIEW:

EXPLAIN ANALYZE SELECT * FROM active_users WHERE streak_days > 7;

WITH (CTE) vs VIEW

-- CTE: временная, в рамках одного запроса
WITH active AS (
    SELECT * FROM users WHERE is_active = true
)
SELECT * FROM active WHERE ...;

-- VIEW: постоянная, видна всем в базе
CREATE VIEW active_users AS
SELECT * FROM users WHERE is_active = true;
VIEWCTE
Область видимостиВся базаОдин запрос
Переиспользование
Права доступа
Хранение данных❌ (обычная)
Хранение данных✅ (матвью)

Практические примеры

Дашборд аналитика

CREATE VIEW analytics.daily_metrics AS
SELECT
    DATE_TRUNC('day', created_at) AS day,
    COUNT(DISTINCT user_id)        AS dau,
    COUNT(*)                       AS events,
    COUNT(*) FILTER (WHERE event = 'purchase') AS purchases
FROM user_events
GROUP BY 1;

Публичный профиль без чувствительных данных

CREATE VIEW public_profiles AS
SELECT
    id,
    display_name,
    avatar_url,
    bio,
    elo_rating,
    streak_days,
    created_at::date AS member_since
FROM users
WHERE is_active = true AND is_public = true;

Итог

Обычный VIEWМатериализованный VIEW
Хранит данные
АктуальностьВсегда свежийНужен REFRESH
Индексы
INSERT/UPDATEИногда
Для чегоАбстракция, безопасностьТяжёлые агрегаты

VIEW — мощный инструмент организации кода и разграничения доступа. Материализованный VIEW — решение для медленных аналитических запросов с допустимой задержкой обновления.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →