Как в запросе 1С проверить вхождение элементов в группы при соединении таблиц

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

В практике разработки на платформе 1С часто возникает задача: сопоставить список элементов с табличной частью или регистром, где в качестве условий отбора могут выступать как конкретные элементы, так и вышестоящие группы иерархического справочника. Типичный пример — справочник фильтров или правил, где указана группа производителей или номенклатуры, а на вход передается массив конкретных позиций.

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

Способ 1. Получение вышестоящих групп через итоги по иерархии

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

Важно помнить об архитектурном ограничении платформы: конструкцию ПОМЕСТИТЬ нельзя использовать в одном предложении с предложением ИТОГИ. Поэтому получение иерархических узлов выполняется отдельным запросом, после чего результат передается во второй запрос в виде массива или таблицы значений.

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


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

ЗапросПроизводители.УстановитьПараметр("МассНоменклатура", МассНоменклатура);
МассивПроизводителей = ЗапросПроизводители.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");

// Шаг 2. Разворачиваем всех родителей с помощью иерархических итогов
ЗапросИерархия = Новый Запрос;
ЗапросИерархия.Текст = 
    "ВЫБРАТЬ
    |	Производители.Ссылка КАК Ссылка
    |ИЗ
    |	Справочник.Производители КАК Производители
    |ГДЕ
    |	Производители.Ссылка В(&МассивПроизводителей)
    |ИТОГИ ПО
    |	Ссылка ТОЛЬКО ИЕРАРХИЯ";

ЗапросИерархия.УстановитьПараметр("МассивПроизводителей", МассивПроизводителей);
ВсеУзлыИерархии = ЗапросИерархия.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");

// Шаг 3. Ищем фильтры, где указаны сами производители или любые их вышестоящие группы
ЗапросФильтры = Новый Запрос;
ЗапросФильтры.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	ФильтрыПроизводители.Ссылка КАК Фильтр
    |ИЗ
    |	Справочник.Фильтры.Производители КАК ФильтрыПроизводители
    |ГДЕ
    |	ФильтрыПроизводители.Производитель В(&ВсеУзлыИерархии)";

ЗапросФильтры.УстановитьПараметр("ВсеУзлыИерархии", ВсеУзлыИерархии);
Результат = ЗапросФильтры.Выполнить().Выгрузить();

Этот метод прост, компактен и перекладывает работу по подъему по дереву на платформенные механизмы, что отлично подходит для выборки уникальных фильтров.

Способ 2. Рекурсивный подъем по иерархии в цикле с временными таблицами

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

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

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

Посмотрим на практический пример программного кода:


МенеджерВТ = Новый МенеджерВременныхТаблиц;

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

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

Если Выборка.Следующий() Тогда
    КоличествоУзлов = Выборка.КоличествоВВыборке;
Иначе
    Возврат;
КонецЕсли;

// Шаг 2. Итеративно поднимаемся до корня справочника
Индекс = 1;
Пока Истина Цикл
    ПредыдущийИндекс = Индекс - 1;
    ИмяПредыдущейВТ = "Иерархия_" + Формат(ПредыдущийИндекс, "ЧГ=");
    ИмяТекущейВТ = "Иерархия_" + Формат(Индекс, "ЧГ=");
    
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Производители.Ссылка КАК Ссылка,
        |	Производители.Родитель КАК Родитель
        |ПОМЕСТИТЬ " + ИмяТекущейВТ + "
        |ИЗ
        |	" + ИмяПредыдущейВТ + " КАК ПредВТ
        |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Производители КАК Производители
        |		ПО ПредВТ.Родитель = Производители.Ссылка
        |ГДЕ
        |	ПредВТ.Родитель <> ЗНАЧЕНИЕ(Справочник.Производители.ПустаяСсылка)
        |
        |ОБЪЕДИНИТЬ
        |
        |ВЫБРАТЬ
        |	ПредВТ.Ссылка,
        |	ПредВТ.Родитель
        |ИЗ
        |	" + ИмяПредыдущейВТ + " КАК ПредВТ;
        |
        |УНИЧТОЖИТЬ " + ИмяПредыдущейВТ + ";
        |
        |ВЫБРАТЬ КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ТекущаяВТ.Ссылка) КАК КоличествоВВыборке 
        |ИЗ " + ИмяТекущейВТ + " КАК ТекущаяВТ";
        
    Выборка = Запрос.Выполнить().Выбрать();
    Выборка.Следующий();
    НовоеКоличество = Выборка.КоличествоВВыборке;
    
    Если НовоеКоличество = КоличествоУзлов Тогда
        Прервать; // Новых родителей нет, дошли до корня дерева
    КонецЕсли;
    
    КоличествоУзлов = НовоеКоличество;
    Индекс = Индекс + 1;
КонецЦикла;

// Шаг 3. Соединяем собранные узлы со справочником фильтров
ИмяФинальнойВТ = "Иерархия_" + Формат(Индекс, "ЧГ=");
Запрос.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	ФильтрыПроизводители.Ссылка КАК Фильтр
    |ИЗ
    |	Справочник.Фильтры.Производители КАК ФильтрыПроизводители
    |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ " + ИмяФинальнойВТ + " КАК ДеревоПроизводителей
    |		ПО ФильтрыПроизводители.Производитель = ДеревоПроизводителей.Ссылка";

ТаблицаФильтров = Запрос.Выполнить().Выгрузить();

Способ 3. Подготовка пар «Номенклатура — Предок» на встроенном языке

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

Вместо сложных конструкций СУБД мы последовательно считываем свойство Родитель и формируем таблицу соответствий. Посмотрим, как это реализовать:


// Таблица соответствия номенклатуры и всех ее групп производителей
ТаблицаСвязей = Новый ТаблицаЗначений;
ТаблицаСвязей.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаСвязей.Колонки.Добавить("УзелПроизводителя", Новый ОписаниеТипов("СправочникСсылка.Производители"));

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

ЗапросИсходный.УстановитьПараметр("МассНоменклатура", МассНоменклатура);
Выборка = ЗапросИсходный.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл
    ТекущийУзел = Выборка.Производитель;
    
    // Поднимаемся по всей ветке иерархии до самого верха
    Пока ЗначениеЗаполнено(ТекущийУзел) Цикл
        НоваяСтрока = ТаблицаСвязей.Добавить();
        НоваяСтрока.Номенклатура = Выборка.Номенклатура;
        НоваяСтрока.УзелПроизводителя = ТекущийУзел;
        
        ТекущийУзел = ТекущийУзел.Родитель;
    КонецЦикла;
КонецЦикла;

// Передаем подготовленную таблицу в финальный запрос
ЗапросСоединения = Новый Запрос;
ЗапросСоединения.Текст = 
    "ВЫБРАТЬ
    |	ТЗ.Номенклатура КАК Номенклатура,
    |	ТЗ.УзелПроизводителя КАК УзелПроизводителя
    |ПОМЕСТИТЬ ВТ_ИерархияНоменклатуры
    |ИЗ
    |	&ТаблицаСвязей КАК ТЗ
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	ВТ_ИерархияНоменклатуры.Номенклатура КАК Номенклатура,
    |	ФильтрыПроизводители.Ссылка КАК Фильтр
    |ИЗ
    |	ВТ_ИерархияНоменклатуры КАК ВТ_ИерархияНоменклатуры
    |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Фильтры.Производители КАК ФильтрыПроизводители
    |		ПО ВТ_ИерархияНоменклатуры.УзелПроизводителя = ФильтрыПроизводители.Производитель";

ЗапросСоединения.УстановитьПараметр("ТаблицаСвязей", ТаблицаСвязей);
Результат = ЗапросСоединения.Выполнить().Выгрузить();

Важные рекомендации при работе с иерархией в запросах

  1. Контроль пустых ссылок: Всегда отсекайте пустые ссылки перед проверками иерархии. Если пустая ссылка попадает в оператор В ИЕРАРХИИ, СУБД воспринимает ее как корень дерева и начинает полное сканирование всей таблицы справочника, что приводит к критическому падению производительности.
  2. Индексация временных таблиц: Если объем данных во временных таблицах превышает несколько сотен строк, обязательно добавляйте предложение ИНДЕКСИРОВАТЬ ПО для полей соединения (Ссылка, Родитель, Производитель).
  3. Выбор подхода: Для небольших массивов (до 100 элементов) быстрее всего работает предварительное разворачивание цепочки родителей на встроенном языке. Для массовой обработки сотен тысяч строк оптимальным архитектурным решением является создание вспомогательного непериодического регистра сведений связей иерархии, обновляемого при записи справочника.
← На главную