Что такое иерархические запросы и зачем они нужны

Иерархические запросы — это специальный тип SQL-запросов, предназначенный для работы с данными, которые имеют древовидную структуру "родитель-потомок". Представьте себе организационную структуру компании, где у каждого сотрудника есть начальник, или файловую систему, где папки содержат подпапки.

Основная проблема, которую решают иерархические запросы

В обычных реляционных таблицах данные хранятся "плоско" — каждая строка содержит ссылку на своего "родителя". Например:

emp_id | name     | manager_id
-------|----------|----------
1      | Иванов   | NULL      (топ-менеджер)
2      | Петров   | 1         (подчиненный Иванова)
3      | Сидоров  | 1         (подчиненный Иванова)
4      | Козлов   | 2         (подчиненный Петрова)

Чтобы получить полную иерархию (кто кому подчиняется на всех уровнях), обычными JOIN'ами это сделать крайне сложно, особенно если уровней вложенности неизвестно заранее.

Основные сферы применения

1. Организационные структуры
- Иерархия сотрудников в компании
- Управленческие цепочки
- Департаменты и подразделения

2. Категории и классификации
- Каталоги товаров (категория → подкатегория → товар)
- Системы тегов и рубрик
- Географические структуры (страна → регион → город)

3. Технические структуры
- Файловые системы (папки и подпапки)
- Меню сайтов и приложений
- Комментарии с ответами и вложениями

4. Финансовые и учетные системы
- Планы счетов в бухгалтерии
- Бюджетные структуры
- Иерархии центров затрат

5. Производственные процессы
- Состав изделий (деталь → сборочная единица → изделие)
- Технологические маршруты
- Зависимости между задачами проекта

Почему это важно

Без иерархических запросов для получения "всех подчиненных Иванова на всех уровнях" пришлось бы:
1. Писать множественные JOIN'ы (но сколько уровней?)
2. Использовать рекурсивные запросы или процедуры
3. Обрабатывать данные в коде приложения

Пример: Каталог товаров

Создадим таблицу для каталога товаров с иерархической структурой:

-- Создание таблицы категорий товаров
CREATE TABLE product_categories (
    category_id INT PRIMARY KEY,
    category_name VARCHAR(100),
    parent_category_id INT,
    category_level VARCHAR(20), -- для наглядности
    FOREIGN KEY (parent_category_id) REFERENCES product_categories(category_id)
);

Объяснение FOREIGN KEY (parent_category_id) REFERENCES product_categories(category_id)

Эта строка создает внешний ключ (foreign key) - связь, которая обеспечивает целостность данных в иерархической структуре.

Что это означает:

FOREIGN KEY (parent_category_id) - объявляет, что столбец parent_category_id является внешним ключом

REFERENCES product_categories(category_id) - указывает, что этот внешний ключ ссылается на столбец category_id в той же таблице product_categories

Как это работает:

  1. Самоссылка: Таблица ссылается сама на себя - это называется рекурсивная связь
  2. Родительско-дочерние отношения: Каждая категория может иметь родительскую категорию
  3. Корневые элементы: Записи верхнего уровня имеют parent_category_id = NULL

Что гарантирует этот constraint:

-- ✅ Разрешено: parent_category_id ссылается на существующую запись
INSERT INTO product_categories VALUES (100, 'Ноутбуки', 10, 'Уровень 2');
-- где 10 - это существующий category_id ('Компьютеры')

-- ✅ Разрешено: parent_category_id = NULL (корневой элемент)
INSERT INTO product_categories VALUES (1, 'Электроника', NULL, 'Корень');

-- ❌ Запрещено: parent_category_id ссылается на несуществующую запись
INSERT INTO product_categories VALUES (200, 'Рубашки', 999, 'Уровень 2');
-- Ошибка: нет записи с category_id = 999

Преимущества использования:

  1. Целостность данных: Нельзя создать "сиротские" записи
  2. Каскадные операции: Можно настроить автоматическое удаление дочерних элементов
  3. Защита от ошибок: СУБД автоматически проверяет корректность связей

Пример нарушения без FOREIGN KEY:

Без этого constraint можно было бы вставить:

-- Без FK это прошло бы, но создало бы "битую" иерархию
INSERT INTO product_categories VALUES (300, 'Новая категория', 99999, 'Уровень 1');

Результат: Категория со ссылкой на несуществующего родителя - иерархия сломана!

SQL код для наполенения тестовой таблицы данными:

-- Заполнение данными
INSERT INTO product_categories VALUES (1, 'Электроника', NULL, 'Корень');
INSERT INTO product_categories VALUES (2, 'Одежда', NULL, 'Корень');
INSERT INTO product_categories VALUES (3, 'Дом и сад', NULL, 'Корень');

-- Подкатегории Электроники
INSERT INTO product_categories VALUES (10, 'Компьютеры', 1, 'Уровень 1');
INSERT INTO product_categories VALUES (11, 'Телефоны', 1, 'Уровень 1');
INSERT INTO product_categories VALUES (12, 'Бытовая техника', 1, 'Уровень 1');

-- Подкатегории Одежды
INSERT INTO product_categories VALUES (20, 'Мужская одежда', 2, 'Уровень 1');
INSERT INTO product_categories VALUES (21, 'Женская одежда', 2, 'Уровень 1');
INSERT INTO product_categories VALUES (22, 'Детская одежда', 2, 'Уровень 1');

-- Подкатегории Компьютеров
INSERT INTO product_categories VALUES (100, 'Ноутбуки', 10, 'Уровень 2');
INSERT INTO product_categories VALUES (101, 'Настольные ПК', 10, 'Уровень 2');
INSERT INTO product_categories VALUES (102, 'Планшеты', 10, 'Уровень 2');

-- Подкатегории Телефонов
INSERT INTO product_categories VALUES (110, 'Смартфоны', 11, 'Уровень 2');
INSERT INTO product_categories VALUES (111, 'Кнопочные телефоны', 11, 'Уровень 2');

-- Подкатегории Мужской одежды
INSERT INTO product_categories VALUES (200, 'Рубашки', 20, 'Уровень 2');
INSERT INTO product_categories VALUES (201, 'Брюки', 20, 'Уровень 2');
INSERT INTO product_categories VALUES (202, 'Костюмы', 20, 'Уровень 2');

-- Подкатегории Смартфонов
INSERT INTO product_categories VALUES (1100, 'iPhone', 110, 'Уровень 3');
INSERT INTO product_categories VALUES (1101, 'Samsung', 110, 'Уровень 3');
INSERT INTO product_categories VALUES (1102, 'Xiaomi', 110, 'Уровень 3');

-- Подкатегории рубашек
INSERT INTO product_categories VALUES (2000, 'Деловые рубашки', 200, 'Уровень 3');
INSERT INTO product_categories VALUES (2001, 'Повседневные рубашки', 200, 'Уровень 3');

Наглядная структура каталога:

├── Электроника (1)
│   ├── Компьютеры (10)
│   │   ├── Ноутбуки (100)
│   │   ├── Настольные ПК (101)
│   │   └── Планшеты (102)
│   ├── Телефоны (11)
│   │   ├── Смартфоны (110)
│   │   │   ├── iPhone (1100)
│   │   │   ├── Samsung (1101)
│   │   │   └── Xiaomi (1102)
│   │   └── Кнопочные телефоны (111)
│   └── Бытовая техника (12)
├── Одежда (2)
│   ├── Мужская одежда (20)
│   │   ├── Рубашки (200)
│   │   │   ├── Деловые рубашки (2000)
│   │   │   └── Повседневные рубашки (2001)
│   │   ├── Брюки (201)
│   │   └── Костюмы (202)
│   ├── Женская одежда (21)
│   └── Детская одежда (22)
└── Дом и сад (3)

Структуру можно получить стаким селектом:

SELECT 
    LEVEL,
    LPAD(' ', (LEVEL-1)*4) || category_name AS tree_view,
    category_id,
    parent_category_id
FROM product_categories
START WITH parent_category_id IS NULL
CONNECT BY PRIOR category_id = parent_category_id
ORDER SIBLINGS BY category_name;

В моей базе Oracle результат выполнения запроса выглядит так:

connect_by_result.png