VIEW: представления

VIEW: представления

На протяжении курса вы писали сложные запросы: многотабличные JOIN, подзапросы, агрегации с GROUP BY. Когда один и тот же запрос нужен регулярно — в нескольких местах кода, в отчётах, в аналитике — его приходится дублировать. Дублирование запросов создаёт проблему: при изменении схемы нужно обновлять каждое вхождение. VIEW (представление) решает это — сохраняет запрос как именованный объект, который можно использовать как таблицу.

Что такое VIEW

VIEW — именованный SELECT-запрос, сохранённый в базе данных. При обращении к представлению PostgreSQL выполняет запрос «под капотом» и возвращает результат, как будто это обычная таблица.

-- Создать представление
CREATE VIEW active_customers AS
SELECT id, name, email, city
FROM customers
WHERE is_active = TRUE;

-- Использовать как таблицу
SELECT * FROM active_customers;
SELECT * FROM active_customers WHERE city = 'Москва';
SELECT COUNT(*) FROM active_customers;

active_customers — не хранит данные. При каждом SELECT PostgreSQL выполняет SELECT id, name, email, city FROM customers WHERE is_active = TRUE и возвращает результат. Данные всегда актуальные.

Создание VIEW

CREATE VIEW имя_представления AS
SELECT ...;

Представление может включать JOIN, WHERE, GROUP BY, подзапросы — любой корректный SELECT:

-- Представление: заказы с именами клиентов
CREATE VIEW orders_with_customers AS
SELECT o.id AS order_id,
       o.status,
       o.total_amount,
       o.created_at,
       c.name AS customer_name,
       c.email AS customer_email
FROM orders o
JOIN customers c ON c.id = o.customer_id;

-- Использование:
SELECT * FROM orders_with_customers
WHERE status = 'pending'
ORDER BY created_at DESC;

Запрос к orders_with_customers делается так же, как к таблице. Присоединение клиента «встроено» в представление — вам не нужно помнить условие JOIN каждый раз.

OR REPLACE: обновить без пересоздания

CREATE OR REPLACE VIEW active_customers AS
SELECT id, name, email, city, phone  -- добавили phone
FROM customers
WHERE is_active = TRUE;

OR REPLACE обновляет существующее представление. Нельзя изменить имена или типы существующих колонок — только добавлять новые в конец или изменять определение при сохранении структуры.

Если нужно изменить колонки — DROP VIEW и CREATE VIEW заново.

IF NOT EXISTS

CREATE VIEW IF NOT EXISTS monthly_sales AS
SELECT DATE_TRUNC('month', created_at) AS month,
       SUM(total_amount) AS revenue,
       COUNT(*) AS order_count
FROM orders
WHERE status = 'completed'
GROUP BY 1
ORDER BY 1;

Пропускает создание, если представление уже существует. Полезно в миграционных скриптах.

DROP VIEW

DROP VIEW active_customers;
DROP VIEW IF EXISTS active_customers;

Удаляет представление. Данные в таблицах не затрагиваются.

Если другое представление зависит от удаляемого:

DROP VIEW active_customers CASCADE;
-- Удалит active_customers и все зависимые от него объекты

Представления для агрегации

Один из самых полезных сценариев — инкапсуляция сложных агрегатных запросов:

-- Статистика по клиентам
CREATE VIEW customer_stats AS
SELECT c.id,
       c.name,
       COUNT(o.id)        AS order_count,
       SUM(o.total_amount) AS lifetime_value,
       MAX(o.created_at)  AS last_order_at
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name;

-- Теперь просто:
SELECT * FROM customer_stats WHERE order_count > 5;
SELECT * FROM customer_stats ORDER BY lifetime_value DESC LIMIT 10;

Без customer_stats каждый такой запрос требовал бы написания JOIN и GROUP BY заново.

Ограничения VIEW

Обычный VIEW — это всегда актуальный результат запроса. Но есть ограничения:

Нельзя добавить индекс: поскольку VIEW не хранит данные, индекс добавить нельзя. При сложных запросах к VIEW производительность определяется индексами базовых таблиц.

Updatable views (обновляемые представления): PostgreSQL позволяет INSERT/UPDATE/DELETE через VIEW при условии, что VIEW основан на одной таблице без агрегаций, DISTINCT, GROUP BY, UNION. Простые VIEW автоматически обновляемые:

-- Это работает (простой VIEW на одну таблицу):
UPDATE active_customers SET city = 'Москва' WHERE id = 42;
-- Эквивалентно: UPDATE customers SET city = 'Москва' WHERE id = 42 AND is_active = TRUE

Для сложных VIEW (JOIN, GROUP BY) — только чтение.

Материализованные представления (MATERIALIZED VIEW)

Обычный VIEW вычисляется заново при каждом обращении. Если запрос тяжёлый (агрегация миллионов строк, сложный JOIN), это медленно. Решение — MATERIALIZED VIEW:

CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT DATE_TRUNC('month', created_at) AS month,
       SUM(total_amount) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1
ORDER BY 1;

MATERIALIZED VIEW сохраняет результат физически на диске — как таблица. При обращении результат берётся напрямую без повторного вычисления запроса. Можно добавлять индексы:

CREATE INDEX idx_monthly_revenue_month ON monthly_revenue(month);

Обновление данных: материализованное представление не обновляется автоматически. Нужно явно обновить командой REFRESH:

REFRESH MATERIALIZED VIEW monthly_revenue;
-- Пересчитывает и сохраняет актуальные данные

Пока идёт REFRESH, представление заблокировано для чтения. Для обновления без блокировки:

REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_revenue;
-- Требует уникального индекса на MATERIALIZED VIEW

Когда использовать: для аналитических дашбордов, отчётов, где допустимо небольшое отставание данных (минуты, часы). Запускать REFRESH по расписанию — через pg_cron или в приложении.

VIEW как слой безопасности

Представления часто используются для ограничения доступа к данным. Вместо прямого доступа к таблице пользователи получают доступ только к представлению:

-- Таблица с чувствительными данными
CREATE TABLE employees (
    id         SERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    salary     NUMERIC(10, 2),
    department TEXT,
    ssn        VARCHAR(11)   -- номер социального страхования
);

-- Представление без чувствительных полей
CREATE VIEW employee_directory AS
SELECT id, name, department
FROM employees;

-- Дать доступ только к представлению
GRANT SELECT ON employee_directory TO hr_readonly_role;
-- REVOKE SELECT ON employees FROM hr_readonly_role;

Пользователи с ролью hr_readonly_role видят имена и отделы, но не зарплаты и SSN. Это стандартная практика — представления как «маска» над чувствительными данными.

Аналогично можно создать представление для конкретного подразделения:

CREATE VIEW it_department_employees AS
SELECT id, name, salary
FROM employees
WHERE department = 'IT';

Если дать пользователю доступ только к it_department_employees, он не сможет увидеть данные других отделов даже при SELECT * FROM it_department_employees — WHERE уже встроен в определение.

WITH CHECK OPTION

При обновляемых представлениях возникает вопрос: что если через VIEW вставить строку, которая не будет видна через это же VIEW?

CREATE VIEW active_customers AS
SELECT * FROM customers WHERE is_active = TRUE;

-- Вставить неактивного клиента через VIEW для активных?
INSERT INTO active_customers (name, email, is_active) VALUES ('Тест', 't@t.com', FALSE);
-- Вставится! Но не будет видна через active_customers

WITH CHECK OPTION запрещает вставку и обновление строк, которые нарушают условие VIEW:

CREATE VIEW active_customers AS
SELECT * FROM customers WHERE is_active = TRUE
WITH CHECK OPTION;

-- Теперь это вызовет ошибку:
INSERT INTO active_customers (name, email, is_active) VALUES ('Тест', 't@t.com', FALSE);
-- ERROR: new row violates check option for view "active_customers"

Просмотр существующих представлений

-- Список всех VIEW
SELECT table_name, view_definition
FROM information_schema.views
WHERE table_schema = 'public';

-- В psql:
\dv        -- список VIEW
\dv+       -- с подробностями
\d+ orders_with_customers  -- определение конкретного VIEW

Практический пример: аналитический слой

-- Текущий инвентарь
CREATE VIEW inventory_status AS
SELECT p.id, p.title, p.category_id, p.price,
       COALESCE(SUM(oi.quantity), 0) AS total_sold
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
LEFT JOIN orders o ON o.id = oi.order_id AND o.status = 'completed'
GROUP BY p.id, p.title, p.category_id, p.price;

-- Топ продаж по категории
CREATE VIEW top_products_by_category AS
SELECT category_id, id AS product_id, title, total_sold
FROM (
    SELECT *, RANK() OVER (PARTITION BY category_id ORDER BY total_sold DESC) AS rnk
    FROM inventory_status
) ranked
WHERE rnk <= 5;

top_products_by_category зависит от inventory_status. При DROP VIEW inventory_status CASCADE оба будут удалены.

Краткий итог

  • CREATE VIEW name AS SELECT ... — сохранить запрос как именованный объект
  • CREATE OR REPLACE VIEW — обновить без удаления
  • DROP VIEW [IF EXISTS] [CASCADE] — удалить представление
  • VIEW вычисляется заново при каждом обращении — данные всегда актуальны
  • MATERIALIZED VIEW — хранит результат физически; требует REFRESH для обновления
  • Можно добавлять индексы к MATERIALIZED VIEW, но не к обычному VIEW
  • VIEW инкапсулирует сложные запросы, снижает дублирование

Что дальше

Вы завершили модуль об индексах, транзакциях и представлениях. Следующий модуль — проектирование базы данных: нормализация, типы связей и антипаттерны, которые делают схему неудобной в поддержке.

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

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике