Как переписать запрос 1С для эффективной проверки вхождения в иерархию без обращения к родителям?

Программист 1С v8.3 (Обычные формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При разработке сложных отчетов или механизмов расчета скидок в 1С программисты часто сталкиваются с необходимостью проверить, входит ли конкретный элемент (например, Номенклатура) в определенную группу или сегмент, который может быть задан на любом уровне иерархии. Классический, но крайне неэффективный способ решения этой задачи — использование длинных цепочек условий ВЫБОР КОГДА с обращением к Родитель, Родитель.Родитель и так далее.

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

Проблема ручного перебора уровней иерархии

В исходной ситуации мы видим запрос, в котором связь таблиц осуществляется через конструкцию ВЫБОР КОГДА. Автор пытается проверить 10 уровней вложенности:


ПО (ВЫБОР
    КОГДА вт.Номенклатура = втЗаказы.Номенклатура
        ИЛИ вт.Номенклатура = втЗаказы.Номенклатура.Родитель
        ИЛИ вт.Номенклатура = втЗаказы.Номенклатура.Родитель.Родитель
        // ... и так далее до 10-го колена
    ТОГДА ИСТИНА
КОНЕЦ)

Проанализируем недостатки такого подхода:

  1. Ограниченность: Если пользователь создаст структуру вложенности в 11 уровней, запрос перестанет работать корректно, а данные будут потеряны без каких-либо системных ошибок.
  2. Производительность: Каждое обращение через «точку» к родителю заставляет СУБД выполнять неявное соединение с таблицей справочника. В данном случае для каждой строки заказа выполняется до 10 дополнительных соединений, что на больших объемах данных приводит к катастрофическому падению скорости.
  3. Нечитаемость: Код становится избыточным и сложным в поддержке.

Конструкция В ИЕРАРХИИ также не всегда применима, особенно когда условие вложенности должно использоваться непосредственно в секции ПО при соединении двух таблиц, так как платформа 1С накладывает ограничения на использование иерархических операторов в условиях связи.

Решение: Метод транзитивного замыкания в запросе

Идея метода заключается в том, чтобы один раз подготовить временную таблицу, которая содержит пары «Предок — Потомок» для всех уровней вложенности. Рассмотрим, как реализовать это элегантно с помощью удвоения длины пути. Этот метод позволяет за 4 шага соединений получить таблицу связей глубиной до 16 уровней ($2^4$).

Выясним причину, почему это работает быстрее: мы переходим от выполнения множества мелких неявных запросов к нескольким массовым операциям над временными таблицами, с которыми SQL-сервер справляется гораздо эффективнее.

Шаг 1: Ограничение выборки и подготовка первого уровня

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


ВЫБРАТЬ РАЗЛИЧНЫЕ
    ЗаказПокупателяТовары.Номенклатура
ПОМЕСТИТЬ втЗаказы
ИЗ
    Документ.ЗаказПокупателя.Товары КАК ЗаказПокупателяТовары
ГДЕ
    ЗаказПокупателяТовары.Ссылка.Проведен;

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

ОБЪЕДИНИТЬ

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

Шаг 2: Бинарное удвоение связей

Теперь мы будем соединять таблицу саму с собой. Если у нас есть связь «А -> Б» и «Б -> В», то после соединения мы получим связь «А -> В». Таким образом, на каждом этапе мы покрываем все более далеких предков.

Рассмотрим пример формирования таблицы ЗамыканияДлины2 (дистанция до 2 уровней):


ВЫБРАТЬ РАЗЛИЧНЫЕ
    ПерваяДуга.НачалоДуги,
    ВтораяДуга.КонецДуги
ПОМЕСТИТЬ ЗамыканияДлины2
ИЗ
    ЗамыканияДлины1 КАК ПерваяДуга
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ЗамыканияДлины1 КАК ВтораяДуга
        ПО ПерваяДуга.КонецДуги = ВтораяДуга.НачалоДуги;

Повторим эту операцию еще несколько раз. Проанализируем: ЗамыканияДлины4 соединяет ЗамыканияДлины2 с собой же, получая связи длиной 4. Следующий шаг даст 8, затем — 16. Этого более чем достаточно для большинства реальных структур справочников 1С.


// ... (аналогичные блоки для 4 и 8 уровней)

ВЫБРАТЬ РАЗЛИЧНЫЕ
    ПерваяДуга.НачалоДуги,
    ВтораяДуга.КонецДуги
ПОМЕСТИТЬ ЗамыканияДлины16
ИЗ
    ЗамыканияДлины8 КАК ПерваяДуга
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ЗамыканияДлины8 КАК ВтораяДуга
        ПО ПерваяДуга.КонецДуги = ВтораяДуга.НачалоДуги;

Шаг 3: Финальное соединение с данными скидок

Теперь, когда у нас есть полная плоская карта иерархии для номенклатуры из заказа, мы можем выполнить обычное ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Больше никаких ВЫБОР КОГДА и обращений через точку!


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

Важные рекомендации по производительности

Чтобы данная схема летала на больших базах, следует обратить внимание на следующие моменты:

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

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

← На главную