← Назад к вопросам
Что такое оператор WITH в БД?
1.3 Junior🔥 141 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
WITH оператор в SQL (CTE — Common Table Expression)
WITH оператор (он же CTE, Common Table Expression) создаёт временную именованную выборку, которую можно использовать в основном запросе. Это делает SQL запросы более читаемыми и переиспользуемыми, особенно для сложной логики.
Базовый синтаксис
-- Базовый WITH
WITH cte_name AS (
SELECT id, name FROM users WHERE is_active = true
)
SELECT * FROM cte_name WHERE id > 10;
-- Несколько CTE (через запятую)
WITH active_users AS (
SELECT id, name FROM users WHERE is_active = true
),
premium_users AS (
SELECT id, name FROM users WHERE subscription = 'premium'
)
SELECT * FROM active_users
INTERSECT
SELECT * FROM premium_users;
Простой пример: фильтрация через WITH
-- БЕЗ WITH: сложнее читается
SELECT
u.id,
u.name,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC;
-- С WITH: более читаемо
WITH user_orders AS (
SELECT
u.id,
u.name,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
)
SELECT * FROM user_orders
WHERE order_count > 5
ORDER BY order_count DESC;
Практический пример: продажи по менеджерам
-- Выборка 1: Продажи по менеджеру
WITH manager_sales AS (
SELECT
m.id as manager_id,
m.name as manager_name,
SUM(s.amount) as total_sales,
COUNT(s.id) as sale_count
FROM managers m
LEFT JOIN sales s ON m.id = s.manager_id
GROUP BY m.id, m.name
)
SELECT * FROM manager_sales
WHERE total_sales > 10000
ORDER BY total_sales DESC;
Рекурсивный WITH (RECURSIVE)
-- Иерархические данные: структура организации
WITH RECURSIVE org_hierarchy AS (
-- Базовый случай: начальник отдела
SELECT
id,
name,
parent_id,
1 as level
FROM employees
WHERE parent_id IS NULL -- Начальники
UNION ALL
-- Рекурсивный случай: подчинённые
SELECT
e.id,
e.name,
e.parent_id,
oh.level + 1
FROM employees e
INNER JOIN org_hierarchy oh ON e.parent_id = oh.id
WHERE oh.level < 5 -- Ограничение глубины
)
SELECT
REPEAT(' ', level - 1) || name as hierarchy,
level
FROM org_hierarchy
ORDER BY level, id;
-- Результат:
-- hierarchy | level
-- CEO | 1
-- CTO | 2
-- Backend Lead | 3
-- Developer | 4
-- Frontend Lead| 3
Другой пример: генерация последовательности
-- Генерировать числа от 1 до 100
WITH RECURSIVE numbers AS (
SELECT 1 as num
UNION ALL
SELECT num + 1
FROM numbers
WHERE num < 100
)
SELECT * FROM numbers;
-- Генерировать даты на месяц
WITH RECURSIVE date_series AS (
SELECT DATE '2024-01-01' as date
UNION ALL
SELECT date + INTERVAL '1 day'
FROM date_series
WHERE date < DATE '2024-12-31'
)
SELECT * FROM date_series;
Пример с Python/SQLAlchemy
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
engine = create_engine('postgresql://user:pass@localhost/db')
# Запрос с WITH
query = text("""
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', order_date) as month,
SUM(amount) as revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) as prev_revenue,
ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month), 2) as growth_percent
FROM monthly_revenue
ORDER BY month;
""")
with Session(engine) as session:
results = session.execute(query).fetchall()
for month, revenue, prev_revenue, growth in results:
print(f"{month}: ${revenue:,.2f} (Growth: {growth}%)")
Window Functions с WITH
-- Ранжирование внутри отделов
WITH employee_salary_rank AS (
SELECT
name,
department,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rank_in_dept,
AVG(salary) OVER (PARTITION BY department) as dept_avg_salary
FROM employees
)
SELECT *
FROM employee_salary_rank
WHERE rank_in_dept <= 3 -- Топ 3 зарплаты в каждом отделе
ORDER BY department, rank_in_dept;
Несколько CTE с зависимостями
WITH
-- CTE 1: активные пользователи
active_users AS (
SELECT id, name, email FROM users WHERE is_active = true
),
-- CTE 2: заказы активных пользователей
user_orders AS (
SELECT
au.id as user_id,
au.name,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent
FROM active_users au
LEFT JOIN orders o ON au.id = o.user_id
GROUP BY au.id, au.name
),
-- CTE 3: топ покупатели
top_customers AS (
SELECT
user_id,
name,
order_count,
total_spent,
RANK() OVER (ORDER BY total_spent DESC) as rank
FROM user_orders
WHERE order_count > 0
)
SELECT * FROM top_customers
WHERE rank <= 10;
Производительность WITH
-- ❓ WITH может быть оптимизирован или нет
-- Зависит от БД (материализация)
-- PostgreSQL: часто материализирует CTE
WITH expensive_query AS (
SELECT * FROM users
WHERE expensive_computation() = true -- Может выполниться один раз
)
SELECT * FROM expensive_query WHERE id > 100;
-- При использовании нескольких раз в основном запросе
-- CTE результат кэшируется
-- Но можно контролировать:
WITH expensive_query AS NOT MATERIALIZED (
-- PostgreSQL 12+: инлайнить вместо материализации
SELECT * FROM users WHERE id > 100
)
SELECT * FROM expensive_query;
Сравнение: WITH vs SUBQUERY
-- Подзапрос (Subquery)
SELECT *
FROM (
SELECT id, name, COUNT(*) as orders
FROM users
GROUP BY id, name
) AS user_orders
WHERE orders > 5;
-- CTE (WITH) — более читаемо
WITH user_orders AS (
SELECT id, name, COUNT(*) as orders
FROM users
GROUP BY id, name
)
SELECT *
FROM user_orders
WHERE orders > 5;
-- Оба работают одинаково по производительности
-- WITH более читаемо для сложных запросов
Реальный пример: Когорта анализ
WITH
-- Когда впервые вошёл каждый пользователь
first_login AS (
SELECT
user_id,
DATE_TRUNC('month', MIN(login_time)) as cohort_month
FROM logins
GROUP BY user_id
),
-- Месяц каждого логина
login_by_month AS (
SELECT
l.user_id,
DATE_TRUNC('month', l.login_time) as login_month,
fl.cohort_month
FROM logins l
JOIN first_login fl ON l.user_id = fl.user_id
),
-- Вычислить месяц от первого логина
cohort_analysis AS (
SELECT
cohort_month,
(EXTRACT(YEAR FROM login_month) - EXTRACT(YEAR FROM cohort_month)) * 12 +
(EXTRACT(MONTH FROM login_month) - EXTRACT(MONTH FROM cohort_month)) as months_since_signup,
COUNT(DISTINCT user_id) as user_count
FROM login_by_month
GROUP BY cohort_month, months_since_signup
)
SELECT
cohort_month,
months_since_signup,
user_count
FROM cohort_analysis
ORDER BY cohort_month, months_since_signup;
Практические советы
-- ✓ ХОРОШО: WITH для читаемости
WITH step1 AS (
SELECT id, name FROM users WHERE status = 'active'
),
step2 AS (
SELECT s1.id, s1.name, COUNT(o.id) as orders
FROM step1 s1
LEFT JOIN orders o ON s1.id = o.user_id
GROUP BY s1.id, s1.name
)
SELECT * FROM step2 WHERE orders > 5;
-- ❌ ПЛОХО: слишком много вложенности без WITH
SELECT * FROM (
SELECT s1.id, s1.name, COUNT(o.id) as orders FROM (
SELECT id, name FROM users WHERE status = 'active'
) s1
LEFT JOIN orders o ON s1.id = o.user_id
GROUP BY s1.id, s1.name
) s2
WHERE orders > 5;
-- ✓ ХОРОШО: сгруппировать логику
WITH -- Данные
raw_data AS (...),
-- Трансформация
transformed_data AS (...),
-- Агрегация
aggregated_data AS (...)
SELECT * FROM aggregated_data;
RECURSIVE WITH — правила
-- Обязательная структура:
WITH RECURSIVE cte_name AS (
-- ANCHOR (базовое условие)
SELECT ... FROM table
WHERE condition
UNION ALL
-- RECURSIVE (рекурсивное условие)
SELECT ... FROM table
JOIN cte_name ON ...
WHERE stop_condition
)
SELECT * FROM cte_name;
-- Правила:
-- 1. Якорь (anchor) должен быть первым
-- 2. Должен быть UNION ALL (не UNION)
-- 3. Рекурсивная часть ссылается на CTE
-- 4. ДОЛЖНО быть стоп условие (WHERE), иначе бесконечный цикл
Ключевые моменты
- WITH создаёт временную выборку (CTE)
- Читаемость улучшается, особенно в сложных запросах
- RECURSIVE для иерархических данных и последовательностей
- Материализация — CTE может быть кэширована или инлайнена
- Несколько CTE через запятую, второй может использовать первый
- WITH vs Subquery — оба работают одинаково, WITH читаемее
- Window Functions отлично работают с WITH
- UNION ALL в RECURSIVE, не UNION