3.07. Представления SQL
Разработчику
Аналитику
Тестировщику
Архитектору
Инженеру
Представления SQL
О представлениях
Порой бывает так, что для просмотра данных нам нужно несколько таблиц, мы, допустим, группируя, объединяя через JOIN, UNION, собираем, агрегируем и делаем каждый раз запрос. К примеру - начальству нужен отчёт, и мы выполняем запрос - но если такой запрос нужен каждый день, нам не обязательно выполнять такой запрос снова и снова - можно сохранить его как представление.
Представления (Views) – это виртуальные таблицы, которые не хранят данные, а только отображают результаты SQL запроса.
Представление – это сохранённый SQL-запрос, который ведёт себя как таблица:
- не содержит данных (только определение запроса);
- всегда показывает актуальные данные из базовых таблиц;
- можно использовать в SELECT, JOIN, WHERE как обычную таблицу.
Это снимок данных, который автоматически обновляется при изменении исходных таблиц.
Это нужно, когда необходимо упростить сложные запросы, и можно скрыть сложную логику за простым именем представления. К примеру, вы написали сложный запрос с вычислением, функциями, джойнами и вывели результат таблицей. Сохранив это как представление с именем, можно будет потом просто обращаться к нему как к таблице, указывая поля из результата. Представления также позволяют дать пользователям доступ только к определённым столбцам или строкам, «скрывая» оригинальные таблицы. И не нужно каждый раз писать один и тот же сложный запрос, просто сохранив его. Если структура таблиц меняется, то можно оставить View со старым именем или структурой.
Синтаксис:
CREATE VIEW view_name AS
SELECT column1, column2, ...
FROM table_name
WHERE condition;
Пример, создание простого представления:
CREATE VIEW active_users AS
SELECT id, name, email
FROM users
WHERE is_active = TRUE;
Теперь можно запрашивать:
SELECT * FROM active_users;
Представления не хранят данные, а всегда выполняют базовый запрос при обращении. Их можно обновлять, они могут быть вложенными, и могут иметь собственные разрешения, независимо от базовых таблиц.
Материализованные представления
В некоторых СУБД (PostgreSQL, Oracle) есть материализованные представления (Materialized Views), которые физически хранят данные, требуют явного обновления, но работают быстрее для сложных запросов:
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT product_id, SUM(amount) as total
FROM sales
GROUP BY product_id;
-- Обновление данных
REFRESH MATERIALIZED VIEW monthly_sales;
Обычное представление - это виртуальная таблица, которая не хранит данные, а каждый раз при запросе выполняет свой SQL-набор команд. Это удобно, когда скрывается сложность или обеспечивается безопасность. Но есть проблема, когда запрос внутри представления будет тяжелый (множество JOIN, агрегаций, подзапросов, миллионы строк) - тогда каждый вызов View будет медленным. Иногда просто не можем позволить себе пересчитывать сложную аналитику каждый раз, когда кто-то захочет посмотреть отчёт. Вот тут на помощь и приходит материализованное представление.
Материализованное представление — это физически сохранённый результат SQL-запроса. В отличие от обычного View, оно хранит данные на диске, как настоящая таблица. Оно не пересчитывается при каждом обращении — оно просто отдаёт уже готовые данные. Но при этом сохраняет идею View: это не просто таблица, а сохранённый запрос, который можно обновить, когда данные в исходных таблицах изменятся.
Давайте рассмотрим отличия:
| Характеристика | Обычное представление | Материализованное представление |
|---|---|---|
| Хранит данные? | Нет. Только определение запроса. | Да. Данные физически сохранены на диске. |
| Скорость чтения | Зависит от сложности запроса (может быть медленной). | Очень быстрая — как у таблицы с индексами. |
| Актуальность | Всегда актуальна (выполняет запрос при обращении). | Устаревает со временем — нужно обновлять вручную. |
| Использование в SELECT, JOIN | Да, как обычная таблица. | Да, как обычная таблица. |
| Поддержка индексов | Нет (индексы работают на базовых таблицах). | Да. Можно создавать индексы на самом MV. |
| Ресурсы | Лёгкое, но нагрузка при каждом вызове. | Требует дискового пространства и времени на обновление. |
Материализованные представления решают одну главную задачу - ускорение сложных запросов за счёт предварительного расчёта. Это стоит использовать для следующих ситуаций:
- Аналитика и отчёты. Вы каждый день строите отчёт «Продажи по регионам за последние 6 месяцев с группировкой по категориям и агрегацией по среднему чеку». Запрос выполняется 8 секунд. Сделайте материализованное представление и обновляйте его раз в ночь, и отчёт будет грузиться за 0.1 секунды.
- Сложные JOIN’ы и агрегации. У вас 5 таблиц, связанных через JOIN, и вы каждый раз считаете SUM, AVG, и прочее. Вынесите это в MV. Добавьте индексы на ключевые поля.
- Интеграции и внешние системы. Внешняя система (например, BI-панель) вредит производительности, если каждый раз дергает сложный запрос. Подайте ей MV. Она читает быстро, а вы контролируете актуальность.
- Кеширование тяжёлых вычислений. Например, рекурсивные запросы (WITH RECURSIVE), работа с иерархиями (деревья, графы), сложные JSON-обработки. Результат можно материализовать и обновлять по расписанию.
Словом, если чувствуете, что работа скрипта медленная, попробуйте вариант с MV.
Разберём на PostgreSQL.
Для создания используется команда CREATE MATERIALIZED VIEW, затем нужно указать SELECT, FROM, JOIN и прочее - результат запроса будет представлять собой то самое представление:
CREATE MATERIALIZED VIEW sales_summary AS
SELECT
region,
product_category,
SUM(sales_amount) AS total_sales,
AVG(order_value) AS avg_order,
COUNT(*) AS order_count
FROM
sales
JOIN products ON sales.product_id = products.id
JOIN regions ON sales.region_id = regions.id
GROUP BY
region, product_category;
После этого с такой sales_summary можно работать как с полноценной таблицей:
SELECT * FROM sales_summary WHERE region = 'Москва';
Поскольку MV — это настоящая таблица, вы можете ускорить её ещё сильнее:
CREATE INDEX idx_sales_summary_region ON sales_summary (region);
CREATE INDEX idx_sales_summary_sales ON sales_summary (total_sales DESC);
Теперь даже сложные WHERE, ORDER BY, JOIN будут работать мгновенно.
Чтобы обновлять данные, используется команда REFRESH MATERIALIZED VIEW. Поскольку данные в MV не обновляются автоматически, нужно вручную синхронизировать их с источником. CONCURRENTLY требует уникального индекса на MV (например, по первичному ключу или уникальному полю). Без него — ошибка.
-- Полное перестроение (блокирует чтение на время обновления)
REFRESH MATERIALIZED VIEW sales_summary;
-- Без блокировки (доступно для чтения во время обновления)
REFRESH MATERIALIZED VIEW CONCURRENTLY sales_summary;
Удаляется материализованное представление через команду DROP. CASCADE — удаляет зависимости, например, индексы и другие View, которые на него ссылаются:
DROP MATERIALIZED VIEW sales_summary;
-- Или, если хотите сохранить данные, но убрать MV:
DROP MATERIALIZED VIEW IF EXISTS sales_summary CASCADE;
Можно создавать триггеры на одну из базовых таблиц, которые будут вызывать REFRESH при изменении данных. К примеру, при любом изменении в таблице employees (вставка, обновление, удаление) MV автоматически будет обновляться. Для этого создаётся функция-обновление:
CREATE OR REPLACE FUNCTION refresh_employee_hierarchy()
RETURNS TRIGGER AS $$
BEGIN
-- Обновляем MV без блокировки (если есть уникальный индекс)
REFRESH MATERIALIZED VIEW CONCURRENTLY employee_hierarchy;
RETURN NULL; -- Для триггеров AFTER не нужен возврат
END;
$$ LANGUAGE plpgsql;
А затем добавляется триггер на таблице:
CREATE TRIGGER trigger_refresh_employee_hierarchy
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH STATEMENT
EXECUTE FUNCTION refresh_employee_hierarchy();
В итоге при любом INSERT, UPDATE, DELETE в employees — employee_hierarchy автоматически обновится. Актуальность данных — почти в реальном времени. Но REFRESH — тяжёлая операция, особенно для больших MV. Если у вас высокая частота изменений — лучше обновлять по расписанию, а не на каждое изменение.
Другие виды представлений
Групповое представление
Групповое представление — это представление, которое содержит агрегатные функции (SUM, COUNT, AVG и т.д.) и/или предложение GROUP BY. Оно позволяет абстрагировать сложную логику агрегации и предоставлять упрощённый доступ к сводным данным.
Пример:
CREATE VIEW department_stats AS
SELECT
department_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
Такое представление может использоваться для аналитики:
SELECT * FROM department_stats WHERE avg_salary > 80000;
Особенность групповых представлений — они не являются обновляемыми (см. ниже), так как основаны на агрегированных данных, не отражающих исходные строки напрямую.
Ограничения представлений
Представления имеют ряд ограничений, зависящих от СУБД, но общие принципы следующие:
- Нельзя напрямую модифицировать агрегированные данные. Представления с GROUP BY, DISTINCT, агрегатами нельзя обновлять.
- Ограничения на состав запроса. Наличие подзапросов в SELECT может сделать представление необновляемым.Использование UNION, LIMIT, HAVING часто исключает возможность обновления.
- Зависимость от базовой структуры. Если изменяется структура базовой таблицы (например, удаляется столбец), представление может перестать работать.
- Производительность. Представления не хранят данные физически (если только это не материализованные представления). Каждый вызов выполняет исходный запрос, что может быть медленно при сложной логике.
- Безопасность. Представления могут использоваться для ограничения доступа к данным (например, скрытие конфиденциальных столбцов), но сами по себе не заменяют полноценного управления правами.
Обновляемые представления
Обновляемое представление — такое, через которое можно выполнять операции INSERT, UPDATE, DELETE, и изменения применяются к базовой таблице.
Условия, при которых представление считается обновляемым (в общем случае):
- Не содержит GROUP BY, DISTINCT, агрегатных функций.
- Не использует UNION, INTERSECT, EXCEPT.
- Включает все обязательные столбцы базовой таблицы (например, NOT NULL без DEFAULT).
- Основано на одной таблице или на соединении, допускающем однозначное сопоставление строк.
Пример:
CREATE VIEW active_employees AS
SELECT id, name, salary, department_id
FROM employees
WHERE status = 'active';
Теперь можно обновлять:
UPDATE active_employees SET salary = 90000 WHERE id = 101;
Изменение применится к строке в таблице employees.
Если представление основано на нескольких таблицах, оно может быть обновляемым только при условии, что изменение затрагивает столбцы одной таблицы, и СУБД может однозначно определить целевую строку.
Проверка представлений (WITH CHECK OPTION)
Ключевая фраза WITH CHECK OPTION гарантирует, что любые изменения через представление не нарушают условия его фильтрации.
Пример:
CREATE VIEW senior_employees AS
SELECT id, name, salary, experience
FROM employees
WHERE experience >= 5
WITH CHECK OPTION;
Попытка:
INSERT INTO senior_employees (id, name, salary, experience)
VALUES (200, 'Ivan', 70000, 3);
...будет отклонена, так как experience = 3 не удовлетворяет условию experience >= 5. Это обеспечивает целостность данных, особенно при использовании представлений как интерфейса ввода.
Исключаемые поля
Под термином "исключаемые поля" в контексте представлений обычно понимаются столбцы базовой таблицы, которые намеренно не включены в представление. Это делается для:
- Сокрытия чувствительных данных.
- Упрощения интерфейса. Исключение технических полей (created_at, updated_by, version) от пользователей, которым они не нужны.
- Оптимизации производительности. Исключение больших полей (BLOB, TEXT), если они не требуются в текущем контексте.
Пример:
CREATE VIEW public_employee_view AS
SELECT id, name, department_id
FROM employees; -- salary, email, phone скрыты
Важно: если представление исключает столбец, который является NOT NULL и не имеет значения по умолчанию, то вставка через это представление будет невозможна, если только представление не определено как обновляемое с явным указанием способа заполнения пропущенных полей (что зависит от СУБД).