В платформе 1С:Предприятие структура иерархических справочников строится по классической модели Adjacency List (список смежности), где каждый элемент ссылается только на своего непосредственного родителя через реквизит Родитель. Из-за отсутствия в языке запросов 1С прямой поддержки рекурсивных запросов (таких как WITH RECURSIVE в классическом SQL), задача получить полную цепочку родителей для каждого элемента «снизу вверх» представляет определенную сложность.
В этой статье мы подробно разберем несколько эффективных практических способов решения этой задачи: от простых соединений до транзитивного замыкания и использования механизмов СКД.
Если в вашей конфигурации максимальное количество уровней вложенности справочника ограничено (например, не превышает 5–10 уровней), самым простым и очевидным решением будет использование левых соединений таблицы самой с собой по цепочке родителей.
Рассмотрим пример запроса, который выбирает элементы справочника Номенклатура и связывает их со всеми вышестоящими родительскими группами:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
НоменклатураРодитель.Ссылка КАК Родитель
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК НоменклатураРодитель
ПО (Номенклатура.Родитель = НоменклатураРодитель.Ссылка)
ИЛИ (Номенклатура.Родитель.Родитель = НоменклатураРодитель.Ссылка)
ИЛИ (Номенклатура.Родитель.Родитель.Родитель = НоменклатураРодитель.Ссылка)
ИЛИ (Номенклатура.Родитель.Родитель.Родитель.Родитель = НоменклатураРодитель.Ссылка)
ИЛИ (Номенклатура.Родитель.Родитель.Родитель.Родитель.Родитель = НоменклатураРодитель.Ссылка)
ГДЕ
НЕ НоменклатураРодитель.Ссылка ЕСТЬ NULL
И НЕ Номенклатура.ЭтоГруппа
Преимущества метода:
Недостатки метода:
ЛЕВОЕ СОЕДИНЕНИЕ) на таблицах с сотнями тысяч записей.Альтернативой этому методу является использование конструкции ОБЪЕДИНИТЬ ВСЕ, где на каждом шаге выбирается конкретный уровень родителя, но это также требует понимания лимита вложенности структуры данных.
Для обхода ограничения на фиксированное число уровней используется алгоритм транзитивного замыкания с использованием временных таблиц. Этот метод основан на бинарном удвоении связей, благодаря чему количество покрываемых уровней иерархии растет по степеням двойки ($2^n$). Это позволяет за минимальное количество шагов связать элементы с родителями на любую глубину.
Проанализируем этот алгоритм шаг за шагом:
Элемент -> Родитель.Связи.Родитель = Связи_Предок.Элемент. В результате получаем связи на расстоянии 2 шага (дедушка). Объединяем их с исходными связями.Ниже представлен пример реализации такого запроса для получения связей глубиной до 8 уровней (3 шага удвоения):
// Шаг 1: Получаем непосредственные связи (уровень 1)
ВЫБРАТЬ
Номенклатура.Ссылка КАК Элемент,
Номенклатура.Родитель КАК Предок
ПОМЕСТИТЬ ВТ_Связи_1
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
Номенклатура.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 2: Получаем связи уровня 2 (удвоение)
ВЫБРАТЬ
Связи1.Элемент КАК Элемент,
ЕСТЬNULL(Связи2.Предок, Связи1.Предок) КАК Предок
ПОМЕСТИТЬ ВТ_Связи_2
ИЗ
ВТ_Связи_1 КАК Связи1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Связи_1 КАК Связи2
ПО Связи1.Предок = Связи2.Элемент
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 3: Получаем связи уровня 4
ВЫБРАТЬ
Связи1.Элемент КАК Элемент,
ЕСТЬNULL(Связи2.Предок, Связи1.Предок) КАК Предок
ПОМЕСТИТЬ ВТ_Связи_4
ИЗ
ВТ_Связи_2 КАК Связи1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Связи_2 КАК Связи2
ПО Связи1.Предок = Связи2.Элемент
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 4: Получаем связи уровня 8 и выводим итоговый результат
ВЫБРАТЬ РАЗЛИЧНЫЕ
Связи1.Элемент КАК Элемент,
ЕСТЬNULL(Связи2.Предок, Связи1.Предок) КАК Предок
ИЗ
ВТ_Связи_4 КАК Связи1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Связи_4 КАК Связи2
ПО Связи1.Предок = Связи2.Элемент
ГДЕ
ЕСТЬNULL(Связи2.Предок, Связи1.Предок) <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
Этот метод является наиболее универсальным, производительным и масштабируемым решением для «чистых» запросов без использования встроенного языка 1С.
Если задача допускает обработку результата на клиенте или сервере во встроенном языке 1С, то встроенный механизм расчета итогов платформы берет на себя всю тяжелую работу по расчету иерархии.
Выясним, как работает этот метод. Мы пишем простой запрос к элементам справочника, но в конце используем конструкцию ИТОГИ ПО ... ИЕРАРХИЯ. Платформа самостоятельно строит дерево данных:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
|ИТОГИ ПО
| Ссылка ИЕРАРХИЯ";
РезультатЗапроса = Запрос.Выполнить();
// Обходим результат по группировкам для получения иерархии
ВыборкаИерархия = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаИерархия.Следующий() Цикл
// Здесь мы получаем доступ к родительским группам на разных уровнях
Сообщить("Группа/Элемент: " + ВыборкаИерархия.Ссылка);
ВыборкаДетальные = ВыборкаИерархия.Выбрать();
Пока ВыборкаДетальные.Следующий() Цикл
// Детальные записи внутри текущей ветки иерархии
КонецЦикла;
КонецЦикла;
Важное примечание: Использование иерархических итогов оптимизировано платформой 1С на уровне обращений к СУБД, однако этот метод неудобно использовать, если результат запроса необходимо напрямую соединить с другими таблицами (например, физическими таблицами остатков или оборотов) внутри одного сложного пакетного запроса.
Система компоновки данных (СКД) предоставляет мощный и гибкий механизм построения собственной иерархии без написания рекурсивного кода. Это особенно актуально для задач, когда нужно вывести данные (например, продажи) и сгруппировать их по родительским группам, причем не по всей стандартной иерархии, а только по тем группам, которые соответствуют определенному признаку (например, реквизит НужнаяГруппа = Истина).
Рассмотрим настройку такой схемы компоновки данных:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
Номенклатура.Родитель КАК Родитель,
Номенклатура.НужнаяГруппа КАК НужнаяГруппа
ИЗ
Справочник.Номенклатура КАК Номенклатура
РодительСсылкаРодитель.Ссылка с типом группировки «Иерархия».НужнаяГруппа = Истина.СКД автоматически выполнит рекурсивные подзапросы для каждого уровня иерархии и построит дерево. При этом уровни, не подходящие под отбор, могут быть скрыты, образуя необходимую разработчику «псевдоиерархию».
Если перед вами стоит задача регулярного получения иерархических отчетов по продажам или расчетов скидок по группам номенклатуры в высоконагруженной базе данных, постоянное вычисление иерархии «на лету» в запросах приведет к существенному снижению производительности системы.
Оптимальным архитектурным паттерном в данном случае является создание вспомогательного регистра сведений (например, ИерархияНоменклатуры).
Структура регистра сведений:
Элемент (Тип: СправочникСсылка.Номенклатура)Родитель (Тип: СправочникСсылка.Номенклатура) — предок любого уровня вложенностиРасстояние (Тип: Число) — 1 для непосредственного родителя, 2 для «дедушки» и т.д.Регистр заполняется программно в модуле объекта справочника Номенклатура в обработчике события ПриЗаписи. При записи элемента (или группы) мы рекурсивно собираем всех его родителей и записываем в этот регистр готовые плоские пары связей.
Теперь получение продаж по всем группам в запросе становится тривиальной задачей, выполняющейся за доли секунды:
ВЫБРАТЬ
Продажи.СуммаОборот КАК Сумма,
Иерархия.Родитель КАК ГруппаНоменклатуры
ИЗ
РегистрНакопления.Продажи.Обороты КАК Продажи
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ИерархияНоменклатуры КАК Иерархия
ПО Продажи.Номенклатура = Иерархия.Элемент
ГДЕ
Иерархия.Родитель.НужнаяГруппа = ИСТИНА
Выбор конкретного решения зависит от условий решаемой задачи: