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 инкапсулирует сложные запросы, снижает дублирование
Что дальше
Вы завершили модуль об индексах, транзакциях и представлениях. Следующий модуль — проектирование базы данных: нормализация, типы связей и антипаттерны, которые делают схему неудобной в поддержке.