Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Рекурсия в запросах БД (CTE и рекурсивные запросы)
Рекурсия в запросах БД — это способ работы с иерархическими данными (дерево категорий, структура подразделений, графы). Реализуется через рекурсивные CTE (Common Table Expression) или рекурсивные запросы.
Что такое рекурсивный запрос?
Рекурсивный запрос позволяет выполнять один и тот же запрос многократно, каждый раз с результатами предыдущего выполнения, пока не будет достигнуто условие выхода.
Структура рекурсивного CTE
WITH RECURSIVE cte_name AS (
-- ЯКОРНАЯ ЧАСТЬ (базовый случай)
SELECT ... FROM table WHERE condition
UNION ALL
-- РЕКУРСИВНАЯ ЧАСТЬ
SELECT ... FROM table
INNER JOIN cte_name ON condition
WHERE condition -- Условие выхода
)
SELECT * FROM cte_name;
Пример 1: Иерархия категорий
-- Таблица категорий с parent_id
CREATE TABLE categories (
id INT PRIMARY KEY,
name VARCHAR(100),
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES categories(id)
);
INSERT INTO categories VALUES
(1, 'Electronics', NULL),
(2, 'Computers', 1),
(3, 'Laptops', 2),
(4, 'Phones', 1),
(5, 'Smartphones', 4);
-- РЕКУРСИВНЫЙ ЗАПРОС: получить всю иерархию
WITH RECURSIVE category_tree AS (
-- Якорная часть: начинаем с корневых категорий
SELECT id, name, parent_id, 0 as level, name as path
FROM categories
WHERE parent_id IS NULL -- Корневые элементы
UNION ALL
-- Рекурсивная часть: находим детей
SELECT
c.id,
c.name,
c.parent_id,
ct.level + 1,
CONCAT(ct.path, ' > ', c.name)
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
WHERE ct.level < 10 -- Защита от бесконечной рекурсии
)
SELECT id, name, level, path FROM category_tree
ORDER BY path;
-- Результат:
-- id | name | level | path
-- 1 | Electronics| 0 | Electronics
-- 2 | Computers | 1 | Electronics > Computers
-- 3 | Laptops | 2 | Electronics > Computers > Laptops
-- 4 | Phones | 1 | Electronics > Phones
-- 5 | Smartphones| 2 | Electronics > Phones > Smartphones
Пример 2: Генерация последовательности чисел
-- Простая рекурсия для генерации чисел от 1 до 5
WITH RECURSIVE numbers AS (
-- Якорная часть
SELECT 1 as num
UNION ALL
-- Рекурсивная часть
SELECT num + 1
FROM numbers
WHERE num < 5 -- Условие выхода
)
SELECT * FROM numbers;
-- Результат: 1, 2, 3, 4, 5
Пример 3: Граф (организационная структура)
-- Таблица сотрудников
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100),
manager_id INT,
FOREIGN KEY (manager_id) REFERENCES employees(id)
);
INSERT INTO employees VALUES
(1, 'CEO John', NULL),
(2, 'Manager Alice', 1),
(3, 'Manager Bob', 1),
(4, 'Developer Charlie', 2),
(5, 'Developer Diana', 2),
(6, 'Developer Eve', 3);
-- Найти всю цепочку подчинения
WITH RECURSIVE org_chart AS (
-- Начинаем с CEO
SELECT id, name, manager_id, 0 as depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Находим прямых подчиненных
SELECT e.id, e.name, e.manager_id, oc.depth + 1
FROM employees e
INNER JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT REPEAT(' ', depth) || name as organization
FROM org_chart
ORDER BY id;
Python пример с SQLAlchemy
from sqlalchemy import select, text, func, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
Base = declarative_base()
class Category(Base):
__tablename__ = 'categories'
id = Column(Integer, primary_key=True)
name = Column(String)
parent_id = Column(Integer, ForeignKey('categories.id'))
# Рекурсивный запрос в Python
def get_category_tree(session: Session, root_id: int):
"""
Получает всю иерархию категорий
"""
query = text("""
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 0 as level
FROM categories
WHERE parent_id = :root_id
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
WHERE ct.level < 5
)
SELECT id, name, level FROM category_tree
""")
results = session.execute(query, {"root_id": root_id})
return results.fetchall()
# Использование
engine = create_engine('postgresql://...')
with Session(engine) as session:
tree = get_category_tree(session, 1)
for cat_id, name, level in tree:
print(' ' * level + name)
Рекурсия vs JOIN: когда использовать?
-- ПЛОХО: множество JOIN для глубокой иерархии
SELECT
c1.name as level1,
c2.name as level2,
c3.name as level3
FROM categories c1
LEFT JOIN categories c2 ON c2.parent_id = c1.id
LEFT JOIN categories c3 ON c3.parent_id = c2.id;
-- ХОРОШО: рекурсивный CTE (один запрос, неограниченная глубина)
WITH RECURSIVE tree AS (
SELECT id, name, parent_id, 0 as level
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, t.level + 1
FROM categories c
INNER JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree;
Защита от бесконечной рекурсии
WITH RECURSIVE numbered_rows AS (
SELECT 1 as n
UNION ALL
SELECT n + 1
FROM numbered_rows
WHERE n < 1000000 -- ОБЯЗАТЕЛЬНО! Условие выхода
)
SELECT * FROM numbered_rows;
Производительность рекурсивных запросов
Плюсы:
- Один запрос вместо N
- Работает для любой глубины иерархии
- Понятная логика
Минусы:
- Медленнее при больших объемах
- Может использовать много памяти
- Не все БД одинаково оптимизируют
# Оптимизация: используй LIMIT для ограничения рекурсии
query = text("""
WITH RECURSIVE tree AS (
...
WHERE level < 10 -- Ограничь глубину
)
SELECT * FROM tree;
""")
Вывод
Рекурсивные запросы (CTE) — это мощный инструмент для работы с иерархическими и графовыми данными. Используй их когда:
- Данные организованы в иерархию (категории, чины, регионы)
- Нужно найти все связанные элементы
- Глубина иерархии неизвестна заранее
Убедись, что:
- Правильно определен якорный запрос
- Есть условие выхода (WHERE)
- Производительность приемлема для твоего случая