Работа с иерархическими структурами в реляционных базах данных — это классическая задача, которая часто вызывает трудности у разработчиков. В экосистеме 1С нам часто требуется сопоставить каждому родительскому узлу (например, подразделению) абсолютно всех его потомков на всех уровнях вложенности. Рассмотрим ситуацию: у нас есть структура, где Подразделение1 включает в себя Подразделение2, а то, в свою очередь, содержит Подразделение3. Для визуализации подобных связей можно использовать инструменты построения дерева конфигурации. Нам необходимо получить результат, где Подразделение1 связано и с Подразделение2, и с Подразделение3 в одной плоской таблице — для этого подойдёт обработка выгрузки данных 1С с сохранением иерархии.
В этой статье мы подробно проанализируем, почему стандартный оператор В ИЕРАРХИИ нельзя использовать внутри условия соединения таблиц, разберем программные методы решения задачи и изучим архитектурные подходы, позволяющие обойти ограничения языка запросов.
Разберем типичную ошибку начинающих разработчиков. Попытка написать запрос вида ПО Таблица2.Ссылка В ИЕРАРХИИ (Таблица1.Ссылка) приведет к ошибке конструктора запроса или времени выполнения. Проанализируем причину: в языке запросов 1С оператор В ИЕРАРХИИ ожидает в качестве аргумента либо конкретную ссылку, либо массив ссылок, либо список значений. Он не может принимать на вход имя колонки другой таблицы в рамках соединения.
На уровне СУБД (SQL) платформа 1С преобразует В ИЕРАРХИИ в сложную конструкцию, использующую технические поля (индексы иерархии), чтобы быстро найти подчиненные элементы. Однако механизм генерации SQL-кода в 1С не поддерживает динамическое вычисление иерархии для каждой строки соединяемой таблицы. Поэтому нам приходится искать обходные пути, используя профессиональные консоли запросов и отчетов для отладки сложных выборок — для этого подойдёт инструментарий разработчика с консолью запросов и СКД.
Если в вашей конфигурации глубина справочника Подразделения ограничена (например, не более 5-6 уровней), можно применить метод последовательных левых соединений или использовать средства интерактивного формирования запросов для получения родителей. Рассмотрим пример запроса, который позволяет собрать иерархию, обращаясь к родителю каждого уровня.
Проанализируем структуру такого запроса:
ВЫБРАТЬ
Элементы.Родитель КАК ОсновнойРодитель,
Элементы.Подгруппа КАК ПодчиненныйЭлемент
ИЗ
(ВЫБРАТЬ
Родители.Ссылка КАК Родитель,
Подчиненные.Ссылка КАК Подгруппа,
Подчиненные.Родитель КАК ПрямойРодитель
ИЗ
Справочник.Подразделения КАК Родители
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Подразделения КАК Подчиненные
ПО (Подчиненные.Родитель = Родители.Ссылка
ИЛИ Подчиненные.Родитель.Родитель = Родители.Ссылка
ИЛИ Подчиненные.Родитель.Родитель.Родитель = Родители.Ссылка
ИЛИ Подчиненные.Родитель.Родитель.Родитель.Родитель = Родители.Ссылка)
ГДЕ
Родители.ПометкаУдаления = ЛОЖЬ
И Подчиненные.Ссылка ЕСТЬ НЕ NULL) КАК Элементы
УПОРЯДОЧИТЬ ПО
ОсновнойРодитель,
ПодчиненныйЭлемент
Важное замечание: Этот метод прост в реализации, но имеет существенный недостаток — он не является универсальным. Если структура разрастется до 7 уровней, элементы седьмого уровня не попадут в выборку для самого верхнего родителя. Кроме того, большое количество обращений через точку (разыменований) в условии ПО может негативно сказаться на производительности при большом объеме данных.
Если нам нужна универсальность для любого количества уровней, мы можем использовать рекурсивную функцию. Этот метод заключается в том, что для каждого найденного подразделения мы снова вызываем ту же функцию поиска, чтобы найти уже его подчиненных.
Рассмотрим пример реализации на встроенном языке:
&НаСервере
Процедура ПолучитьВсехПотомков(ИсходныйРодитель, СписокРезультат)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Подразделения.Ссылка КАК Ссылка
|ИЗ
| Справочник.Подразделения КАК Подразделения
|ГДЕ
| Подразделения.Родитель = &Родитель";
Запрос.УстановитьПараметр("Родитель", ИсходныйРодитель);
РезультатЗапроса = Запрос.Выполнить().Выбрать();
Пока РезультатЗапроса.Следующий() Цикл
// Добавляем найденный элемент в общий список
СписокРезультат.Добавить(РезультатЗапроса.Ссылка);
// Рекурсивно вызываем поиск для найденного подразделения
ПолучитьВсехПотомков(РезультатЗапроса.Ссылка, СписокРезультат);
КонецЦикла;
КонецПроцедуры
Выясним причину, по которой этот метод не рекомендуется для больших справочников. Здесь возникает проблема "N+1 запроса": если в вашей иерархии 1000 подразделений, система выполнит 1000 отдельных запросов к базе данных. Это создаст колоссальную нагрузку на сервер и сеть.
Чтобы избежать сотен запросов к базе данных, мы можем применить более эффективный подход: получить все данные одним запросом и обработать их в памяти сервера. Выгрузим весь справочник в таблицу значений и будем использовать метод НайтиСтроки().
Разберем этот алгоритм по шагам:
Родитель.ТаблицаПодразделений.НайтиСтроки(Новый Структура("Родитель", ТекущийРодитель)).Это на порядки быстрее, так как обращение к базе данных происходит всего один раз.
Если задача стоит в рамках построения отчета, то СКД — самый мощный инструмент для работы с иерархией. В СКД есть механизм Связи наборов данных, который позволяет реализовать рекурсию на уровне платформы.
Проанализируем настройку такой связи:
Ссылка, приемник связи — Родитель.Платформа автоматически построит дерево любой вложенности. При этом алгоритмы СКД оптимизированы для подобных операций и работают быстрее, чем ручной перебор в коде.
Для высоконагруженных систем, где запросы к полной иерархии выполняются постоянно (например, в правах доступа или сложных управленческих расчетах), рекомендуется использовать архитектурный паттерн Closure Table.
Суть метода заключается в создании дополнительного регистра сведений ИерархияПодразделений с измерениями Родитель и Потомок. В этот регистр записываются все возможные связи:
При таком подходе получение всей иерархии для Подразделения 1 превращается в простейший запрос к этому регистру:
ВЫБРАТЬ Потомок ИЗ РегистрСведений.ИерархияПодразделений ГДЕ Родитель = &НужноеПодразделение
Преимущество: Мгновенное получение результата одним запросом без использования В ИЕРАРХИИ и без рекурсии.
Недостаток: Необходимо программно поддерживать актуальность этого регистра при изменении родителей в справочнике подразделений.
Выбор метода зависит от ваших целей. Рассмотрим краткие рекомендации:
Помните, что рекурсивные запросы в цикле (как в Сообщении 18) — это крайняя мера, допустимая только на очень маленьких объемах данных или в разовых обработках.