Как эффективно установить значения для элементов иерархического справочника с учетом приоритетов вложенных групп

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

При работе с крупными базами данных в системе 1С:Предприятие часто возникает задача массовой обработки элементов справочника. Одной из наиболее сложных вариаций этой задачи является установка значений (например, цен, категорий или признаков) для элементов справочника с учетом их положения в иерархии. Ситуация осложняется, когда справочник содержит сотни тысяч элементов (например, Номенклатура объемом 500 000 позиций), а правила установки значений могут пересекаться: более низкая группа в иерархии должна иметь приоритет над вышестоящей.

В этой статье мы подробно разберем, как реализовать подобный механизм, избежав типичных ошибок производительности, таких как запросы в цикле или избыточное потребление оперативной памяти. Мы проанализируем различные подходы — от работы с коллекциями в памяти до продвинутых техник использования запросов.

Постановка задачи и анализ ограничений

Рассмотрим классический пример: у нас есть иерархический справочник без ограничения глубины. Пользователь задает таблицу правил:

  1. Группа "Бытовая техника" — Значение "НДС 20%".
  2. Группа "Холодильники" (входит в "Бытовую технику") — Значение "НДС 10%".

Согласно логике приоритетов, все товары внутри "Холодильников" должны получить ставку 10%, а остальные товары в "Бытовой технике" — 20%. Если товаров 500 000, стандартный перебор каждого элемента с проверкой его родителей через В ИЕРАРХИИ в цикле приведет к критическому замедлению системы.

Решение 1: Построение дерева родителей в памяти через хэш-таблицы

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

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

Шаг 2: Создание структуры быстрого поиска. Вместо того чтобы для каждого товара заново вычислять всю цепочку родителей, мы построим «перевернутое дерево». Разберем, как это реализовать программно:


// Создаем соответствие, где Ключ - ссылка на группу, 
// а Значение - структура с информацией о ее родителе и установленном значении.
КэшИерархии = Новый Соответствие;

// В цикле по результатам запроса заполняем только уникальные группы
Для Каждого Строка Из РезультатЗапросаГруппы Цикл
    Если КэшИерархии[Строка.Ссылка] = Неопределено Тогда
        ДанныеУзла = Новый Структура("Родитель, ЗначениеПрямое", Строка.Родитель, Неопределено);
        // Если для этой группы пользователь задал значение, фиксируем его
        Если ПравилаПользователя.Найти(Строка.Ссылка) Тогда
            ДанныеУзла.ЗначениеПрямое = ЗначениеИзПравил;
        КонецЕсли;
        КэшИерархии.Вставить(Строка.Ссылка, ДанныеУзла);
    КонецЕсли;
КонецЦикла;

Шаг 3: Алгоритм определения значения. Теперь для любого элемента мы можем мгновенно «подняться» до самого верха, переходя по ссылкам в Соответствии. Так как Соответствие и Структура в 1С работают как хэш-таблицы, поиск по ключу происходит практически мгновенно (сложность O(1)). Мы идем от элемента вверх: первое найденное значение в цепочке родителей и будет самым приоритетным (так как оно принадлежит самой низкой группе в иерархии).

Решение 2: Использование уровней иерархии в запросе

Рассмотрим альтернативный способ, который позволяет переложить часть вычислений на SQL-сервер. Этот метод эффективен, когда нам нужно получить итоговую таблицу соответствий «Товар — Значение» без сложной обработки в коде.

Выясним причину, почему обычное соединение не подходит: один товар может входить в несколько групп из правил (например, и в "Бытовую технику", и в "Холодильники"). Нам нужно выбрать ту связь, где уровень вложенности группы максимален.

  1. Создадим временную таблицу ВТ_Правила, куда поместим ссылки на группы и их уровень в иерархии справочника (функция Уровень() в запросе или заранее вычисленное число).
  2. Выполним соединение справочника с этой таблицей по условию Номенклатура.Ссылка В ИЕРАРХИИ (ВТ_Правила.Группа).
  3. Применим группировку: для каждой номенклатуры выберем МАКСИМУМ(ВТ_Правила.Уровень).
  4. Результат соединим еще раз с ВТ_Правила по полям «Группа» и «Максимальный уровень», чтобы получить итоговое значение.

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


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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_МаксимальныеУровни.Товар,
    ВТ_Правила.Значение
ИЗ
    ВТ_МаксимальныеУровни КАК ВТ_МаксимальныеУровни
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Правила КАК ВТ_Правила
        ПО ВТ_МаксимальныеУровни.ПриоритетныйУровень = ВТ_Правила.Уровень
        И ВТ_МаксимальныеУровни.Товар В ИЕРАРХИИ (ВТ_Правила.Группа)

Оптимизация записи в периодический регистр сведений

Когда мы рассчитали значения для 500 000 элементов, встает вопрос их записи. Запись такого объема данных через МенеджерЗаписи в цикле — это верный способ остановить работу базы на несколько часов. Проанализируем, как сделать это правильно.

Использование наборов записей. Вместо записи по одной строке, следует использовать РегистрСведенийНаборЗаписей. Однако записывать все 500к одним набором тоже нельзя — это вызовет переполнение лога транзакций и блокировку таблицы _InfoRg.

Рассмотрим оптимальную стратегию записи:

Пример кода для эффективной записи порциями:


Набор = РегистрыСведений.ЗначенияНоменклатуры.СоздатьНаборЗаписей();
Счетчик = 0;
Для Каждого СтрокаРезультата Из ТаблицаРезультатов Цикл
    НоваяЗапись = Набор.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаРезультата);
    Счетчик = Счетчик + 1;
    
    Если Счетчик % 1000 = 0 Тогда
        Набор.Записать(Ложь); // Ложь - чтобы не замещать всё, если нет отбора
        Набор.Очистить();
    КонецЕсли;
КонецЦикла;
Если Набор.Количество() > 0 Тогда
    Набор.Записать(Ложь);
КонецЕсли;

Дополнительные рекомендации

Для обеспечения максимальной отзывчивости интерфейса и стабильности системы, разберем еще два важных момента:

1. Фоновое выполнение. Операции над 500к объектов должны всегда выполняться в фоновом задании. Это позволит пользователю продолжать работу, пока сервер выполняет тяжелые расчеты. Используйте процедуру ФоновыеЗадания.Выполнить().

2. Метод Materialized Path (Полный код). Если структура справочника меняется редко, можно использовать поиск по шаблону кода. Если код группы "01", а подгруппы "01.05", то запрос ГДЕ Код ПОДОБНО "01.05%" отработает на порядок быстрее, чем В ИЕРАРХИИ, так как задействует индекс по полю Код. Это продвинутая техника, требующая строгого контроля за уникальностью и структурой кодов.

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

← На главную