При оптимизации справочников, подготовке выгрузок на сайт или настройке обменов периодически возникает типовая задача: найти все группы (папки) справочника Номенклатура, внутри которых нет ни одной позиции с определенным реквизитом или признаком (например, нет товаров в наличии, нет элементов с признаком Услуга = Ложь или без признака активности). Сложность заключается в том, что в справочнике присутствует многоуровневая иерархия, и пустой должна считаться группа только в том случае, если ни на одном из вложенных уровней нет удовлетворяющих условию элементов.
Разберем специфику работы платформы с иерархическими связями в запросах и рассмотрим проверенные практические способы решения этой задачи.
Проанализируем типичную ошибку начинающих разработчиков. Часто пытаются отобрать папки, исключая непосредственных родителей элементов с нужным признаком:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
Номенклатура.ЭтоГруппа = ИСТИНА
И Номенклатура.Ссылка НЕ В
(ВЫБРАТЬ
Элементы.Родитель
ИЗ
Справочник.Номенклатура КАК Элементы
ГДЕ
Элементы.ОпределенныйРеквизит = ИСТИНА)
Такой подход проверяет только прямого родителя. Если структура выглядит как Родитель1 -> Родитель2 -> Родитель3 -> Товар, то Родитель3 будет исключен, а вот вышестоящие папки Родитель1 и Родитель2 попадут в результат как пустые, хотя фактически внутри них находится товар с нужным признаком.
Кроме того, язык запросов 1С имеет жесткое ограничение: оператор В ИЕРАРХИИ нельзя использовать в секции соединения таблиц (ПО Таблица1.Ссылка В ИЕРАРХИИ Таблица2.Ссылка). А вызов запроса с В ИЕРАРХИИ (&ТекущаяПапка) в цикле для каждой группы является серьезной ошибкой производительности, приводящей к многочисленным медленным обращениям к базе данных.
Рассмотрим наиболее производительный и надежный алгоритм, который работает при любой глубине вложенности папок. Вместо поиска пустых групп сверху вниз, мы сначала найдем все папки, в которых нужные элементы гарантированно есть, поднимаясь от элементов к корню дерева, а затем исключим их из общего списка групп.
Разберем по шагам логику работы:
Родитель до самого корня каталога и сохраняем все пройденные группы в объекте Соответствие.Посмотрим на пример реализации на встроенном языке:
// Шаг 1. Получаем непосредственных родителей подходящих элементов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Номенклатура.Родитель КАК Родитель
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
| И Номенклатура.ОпределенныйРеквизит = ИСТИНА
| И Номенклатура.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)";
Выборка = Запрос.Выполнить().Выбрать();
// Шаг 2. Собираем всех родителей по всей глубине иерархии снизу вверх
ЗанятыеПапки = Новый Соответствие;
Пока Выборка.Следующий() Цикл
ТекущийРодитель = Выборка.Родитель;
Пока ЗначениеЗаполнено(ТекущийРодитель) Цикл
Если ЗанятыеПапки[ТекущийРодитель] <> Неопределено Тогда
// Если узел уже обработан ранее, вышестоящие ветки уже в коллекции
Прервать;
КонецЕсли;
ЗанятыеПапки.Вставить(ТекущийРодитель, Истина);
ТекущийРодитель = ТекущийРодитель.Родитель;
КонецЦикла;
КонецЦикла;
// Шаг 3. Получаем все группы справочника и отбираем только пустые
ЗапросВсеГруппы = Новый Запрос;
ЗапросВсеГруппы.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Группа
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.ЭтоГруппа = ИСТИНА";
ВыборкаГрупп = ЗапросВсеГруппы.Выполнить().Выбрать();
ПустыеГруппы = Новый Массив;
Пока ВыборкаГрупп.Следующий() Цикл
Если ЗанятыеПапки[ВыборкаГрупп.Группа] = Неопределено Тогда
ПустыеГруппы.Добавить(ВыборкаГрупп.Группа);
КонецЕсли;
КонецЦикла;
Данный вариант работает с линейной сложностью, не блокирует таблицы СУБД и мгновенно отрабатывает даже на каталогах объемом в сотни тысяч позиций.
Рассмотрим вариант, предложенный участниками обсуждения, основанный на агрегации данных средствами платформы с помощью секции ИТОГИ ПО ... ИЕРАРХИЯ.
Посмотрим на пример запроса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Номенклатура.Родитель КАК Родитель,
КОЛИЧЕСТВО(ВЫБОР
КОГДА Номенклатура.ОпределенныйРеквизит = ИСТИНА
ТОГДА Номенклатура.Ссылка
КОНЕЦ) КАК КоличествоНужных
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
СГРУППИРОВАТЬ ПО
Номенклатура.Родитель
ИТОГИ
СУММА(КоличествоНужных)
ПО
Родитель ИЕРАРХИЯ
Особенности этого метода:
ИТОГИ платформы 1С автоматически рассчитывает агрегаты снизу вверх по дереву иерархии.Выгрузить() с сохранением иерархических сумм — его необходимо обходить с помощью метода Выборка.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией).КоличествоНужных = 0, то данная папка (и все ее дочерние узлы) гарантированно не содержит искомых элементов.Если бизнес-логика базы данных предполагает фиксированную максимальную глубину каталога (например, не более 3–4 уровней вложенности), задачу можно решить одним пакетным запросом без кода на встроенном языке.
Выясним, как составить такой запрос:
ВЫБРАТЬ
Номенклатура.Родитель КАК Папка
ПОМЕСТИТЬ ВТ_НепустыеПапки
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
И Номенклатура.ОпределенныйРеквизит = ИСТИНА
И Номенклатура.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Номенклатура.Родитель.Родитель
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
И Номенклатура.ОпределенныйРеквизит = ИСТИНА
И Номенклатура.Родитель.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Номенклатура.Родитель.Родитель.Родитель
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
И Номенклатура.ОпределенныйРеквизит = ИСТИНА
И Номенклатура.Родитель.Родитель.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Номенклатура.Ссылка КАК ПустаяПапка
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
Номенклатура.ЭтоГруппа = ИСТИНА
И НЕ Номенклатура.Ссылка В
(ВЫБРАТЬ РАЗЛИЧНЫЕ
ВТ.Папка
ИЗ
ВТ_НепустыеПапки КАК ВТ)
Этот способ удобен, когда результат требуется получить строго в рамках одного запроса (например, в динамическом списке или в отчете без использования встроенного языка), однако он ограничен заранее заданной максимальной глубиной уровней вложенности.