Как найти пустые группы номенклатуры с учетом иерархии в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При оптимизации справочников, подготовке выгрузок на сайт или настройке обменов периодически возникает типовая задача: найти все группы (папки) справочника Номенклатура, внутри которых нет ни одной позиции с определенным реквизитом или признаком (например, нет товаров в наличии, нет элементов с признаком Услуга = Ложь или без признака активности). Сложность заключается в том, что в справочнике присутствует многоуровневая иерархия, и пустой должна считаться группа только в том случае, если ни на одном из вложенных уровней нет удовлетворяющих условию элементов.

Разберем специфику работы платформы с иерархическими связями в запросах и рассмотрим проверенные практические способы решения этой задачи.

Почему стандартный запрос «в лоб» не решает задачу

Проанализируем типичную ошибку начинающих разработчиков. Часто пытаются отобрать папки, исключая непосредственных родителей элементов с нужным признаком:


ВЫБРАТЬ
    Номенклатура.Ссылка КАК Ссылка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    Номенклатура.ЭтоГруппа = ИСТИНА
    И Номенклатура.Ссылка НЕ В
        (ВЫБРАТЬ
            Элементы.Родитель
        ИЗ
            Справочник.Номенклатура КАК Элементы
        ГДЕ
            Элементы.ОпределенныйРеквизит = ИСТИНА)

Такой подход проверяет только прямого родителя. Если структура выглядит как Родитель1 -> Родитель2 -> Родитель3 -> Товар, то Родитель3 будет исключен, а вот вышестоящие папки Родитель1 и Родитель2 попадут в результат как пустые, хотя фактически внутри них находится товар с нужным признаком.

Кроме того, язык запросов 1С имеет жесткое ограничение: оператор В ИЕРАРХИИ нельзя использовать в секции соединения таблиц (ПО Таблица1.Ссылка В ИЕРАРХИИ Таблица2.Ссылка). А вызов запроса с В ИЕРАРХИИ (&ТекущаяПапка) в цикле для каждой группы является серьезной ошибкой производительности, приводящей к многочисленным медленным обращениям к базе данных.

Способ 1. Инверсия поиска «снизу вверх» (запрос + обработка в памяти)

Рассмотрим наиболее производительный и надежный алгоритм, который работает при любой глубине вложенности папок. Вместо поиска пустых групп сверху вниз, мы сначала найдем все папки, в которых нужные элементы гарантированно есть, поднимаясь от элементов к корню дерева, а затем исключим их из общего списка групп.

Разберем по шагам логику работы:

  1. Одним быстрым запросом выбираем всех уникальных непосредственных родителей для элементов, у которых требуемый реквизит заполнен или истинен.
  2. Для каждого найденного родителя программно поднимаемся вверх по цепочке через свойство Родитель до самого корня каталога и сохраняем все пройденные группы в объекте Соответствие.
  3. Вторым простым запросом (или из исходной выборки всех групп) получаем полный список папок справочника и отбираем те, ключей которых нет в нашем соответствии.

Посмотрим на пример реализации на встроенном языке:


// Шаг 1. Получаем непосредственных родителей подходящих элементов
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	Номенклатура.Родитель КАК Родитель
    |ИЗ
    |	Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |	НЕ Номенклатура.ЭтоГруппа
    |	И Номенклатура.ОпределенныйРеквизит = ИСТИНА
    |	И Номенклатура.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)";

Выборка = Запрос.Выполнить().Выбрать();

// Шаг 2. Собираем всех родителей по всей глубине иерархии снизу вверх
ЗанятыеПапки = Новый Соответствие;

Пока Выборка.Следующий() Цикл
    ТекущийРодитель = Выборка.Родитель;
    Пока ЗначениеЗаполнено(ТекущийРодитель) Цикл
        Если ЗанятыеПапки[ТекущийРодитель] <> Неопределено Тогда
            // Если узел уже обработан ранее, вышестоящие ветки уже в коллекции
            Прервать;
        КонецЕсли;
        
        ЗанятыеПапки.Вставить(ТекущийРодитель, Истина);
        ТекущийРодитель = ТекущийРодитель.Родитель;
    КонецЦикла;
КонецЦикла;

// Шаг 3. Получаем все группы справочника и отбираем только пустые
ЗапросВсеГруппы = Новый Запрос;
ЗапросВсеГруппы.Текст = 
    "ВЫБРАТЬ
    |	Номенклатура.Ссылка КАК Группа
    |ИЗ
    |	Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |	Номенклатура.ЭтоГруппа = ИСТИНА";

ВыборкаГрупп = ЗапросВсеГруппы.Выполнить().Выбрать();
ПустыеГруппы = Новый Массив;

Пока ВыборкаГрупп.Следующий() Цикл
    Если ЗанятыеПапки[ВыборкаГрупп.Группа] = Неопределено Тогда
        ПустыеГруппы.Добавить(ВыборкаГрупп.Группа);
    КонецЕсли;
КонецЦикла;

Данный вариант работает с линейной сложностью, не блокирует таблицы СУБД и мгновенно отрабатывает даже на каталогах объемом в сотни тысяч позиций.

Способ 2. Использование итогов по иерархии в запросе

Рассмотрим вариант, предложенный участниками обсуждения, основанный на агрегации данных средствами платформы с помощью секции ИТОГИ ПО ... ИЕРАРХИЯ.

Посмотрим на пример запроса:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Номенклатура.Родитель КАК Родитель,
    КОЛИЧЕСТВО(ВЫБОР
            КОГДА Номенклатура.ОпределенныйРеквизит = ИСТИНА
                ТОГДА Номенклатура.Ссылка
        КОНЕЦ) КАК КоличествоНужных
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    НЕ Номенклатура.ЭтоГруппа
СГРУППИРОВАТЬ ПО
    Номенклатура.Родитель
ИТОГИ
    СУММА(КоличествоНужных)
ПО
    Родитель ИЕРАРХИЯ

Особенности этого метода:

  1. Секция ИТОГИ платформы 1С автоматически рассчитывает агрегаты снизу вверх по дереву иерархии.
  2. Результат такого запроса нельзя выгрузить в плоскую таблицу через Выгрузить() с сохранением иерархических сумм — его необходимо обходить с помощью метода Выборка.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией).
  3. Если у узла иерархии результирующее значение КоличествоНужных = 0, то данная папка (и все ее дочерние узлы) гарантированно не содержит искомых элементов.

Способ 3. Запрос с разворачиванием фиксированной глубины иерархии

Если бизнес-логика базы данных предполагает фиксированную максимальную глубину каталога (например, не более 3–4 уровней вложенности), задачу можно решить одним пакетным запросом без кода на встроенном языке.

Выясним, как составить такой запрос:


ВЫБРАТЬ
    Номенклатура.Родитель КАК Папка
ПОМЕСТИТЬ ВТ_НепустыеПапки
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    НЕ Номенклатура.ЭтоГруппа
    И Номенклатура.ОпределенныйРеквизит = ИСТИНА
    И Номенклатура.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Номенклатура.Родитель.Родитель
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    НЕ Номенклатура.ЭтоГруппа
    И Номенклатура.ОпределенныйРеквизит = ИСТИНА
    И Номенклатура.Родитель.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Номенклатура.Родитель.Родитель.Родитель
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    НЕ Номенклатура.ЭтоГруппа
    И Номенклатура.ОпределенныйРеквизит = ИСТИНА
    И Номенклатура.Родитель.Родитель.Родитель <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Номенклатура.Ссылка КАК ПустаяПапка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    Номенклатура.ЭтоГруппа = ИСТИНА
    И НЕ Номенклатура.Ссылка В
        (ВЫБРАТЬ РАЗЛИЧНЫЕ
            ВТ.Папка
        ИЗ
            ВТ_НепустыеПапки КАК ВТ)

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

← На главную