Как оптимизировать расчет скидок по иерархии номенклатуры в 1С для большого объема данных?

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

При разработке торговых систем на платформе 1С:Предприятие одной из классических задач является расчет автоматических скидок. Часто скидки назначаются не на каждый конкретный товар, а на целые группы (папки) номенклатуры. При этом логика поиска скидки в документе или отчете выглядит следующим образом: система ищет скидку непосредственно для самого товара; если её нет, обращается к его родителю; если нет у родителя — к родителю родителя, и так далее до самого верха иерархии.

Если для одного документа такая логика, реализованная через цикл в общем модуле, работает приемлемо, то при попытке построить отчет (например, «Прайс-лист со скидками для клиента») на 20 000 позиций система сталкивается с критическим падением производительности. Вызов функции общего модуля в цикле для каждой из 20 000 строк порождает десятки тысяч запросов к базе данных, что приводит к зависанию системы. Создание вспомогательного регистра с предрассчитанными скидками (для связок «20 000 товаров * 7 000 клиентов = 140 миллионов записей») также не всегда жизнеспособно из-за колоссального объема данных и нагрузки на регламентные задания.

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

---

Почему стандартный поиск в цикле работает медленно?

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

  1. Происходит постоянное переключение контекста между сервером приложений 1С и СУБД.
  2. СУБД вынуждена выполнять тысячи мелких запросов типа SELECT вместо одного пакетного запроса, который обрабатывает все данные за один проход.
  3. Неэффективно используются кэши СУБД и платформы.

Наша цель — перенести всю логику поиска «ближайшего родителя со скидкой» на уровень СУБД и выполнить расчет за один единый запрос.

---

Решение 1. Запрос с фиксированным количеством уровней иерархии (Метод «ВЫБОР КОГДА»)

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

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

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


ВЫБРАТЬ
    Номенклатура.Ссылка КАК Товар,
    ЕСТЬNULL(СкидкаТовара.Процент, 0) КАК СкидкаНоменклатура,
    ЕСТЬNULL(СкидкаРодителя1.Процент, 0) КАК СкидкаРодитель,
    ЕСТЬNULL(СкидкаРодителя2.Процент, 0) КАК СкидкаРодитель2,
    ЕСТЬNULL(СкидкаРодителя3.Процент, 0) КАК СкидкаРодитель3,
    ЕСТЬNULL(СкидкаРодителя4.Процент, 0) КАК СкидкаРодитель4,
    ЕСТЬNULL(СкидкаРодителя5.Процент, 0) КАК СкидкаРодитель5,
    ЕСТЬNULL(СкидкаРодителя6.Процент, 0) КАК СкидкаРодитель6,
    ЕСТЬNULL(СкидкаРодителя7.Процент, 0) КАК СкидкаРодитель7
ПОМЕСТИТЬ ВТВсеСкидки
ИЗ
    Справочник.Номенклатура КАК Номенклатура
    
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК СкидкаТовара
        ПО Номенклатура.Ссылка = СкидкаТовара.ПолучательСкидки И СкидкаТовара.Контрагент = &Контрагент
        
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК СкидкаРодителя1
        ПО Номенклатура.Родитель = СкидкаРодителя1.ПолучательСкидки И СкидкаРодителя1.Контрагент = &Контрагент
        
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК СкидкаРодителя2
        ПО Номенклатура.Родитель.Родитель = СкидкаРодителя2.ПолучательСкидки И СкидкаРодителя2.Контрагент = &Контрагент
        
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК СкидкаРодителя3
        ПО Номенклатура.Родитель.Родитель.Родитель = СкидкаРодителя3.ПолучательСкидки И СкидкаРодителя3.Контрагент = &Контрагент
        
    // И так далее для всех 7 уровней...
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТВсеСкидки.Товар,
    ВЫБОР
        КОГДА ВТВсеСкидки.СкидкаНоменклатура <> 0
            ТОГДА ВТВсеСкидки.СкидкаНоменклатура
        ИНАЧЕ ВЫБОР
            КОГДА ВТВсеСкидки.СкидкаРодитель <> 0
                ТОГДА ВТВсеСкидки.СкидкаРодитель
            ИНАЧЕ ВЫБОР
                КОГДА ВТВсеСкидки.СкидкаРодитель2 <> 0
                    ТОГДА ВТВсеСкидки.СкидкаРодитель2
                ИНАЧЕ ВЫБОР
                    КОГДА ВТВсеСкидки.СкидкаРодитель3 <> 0
                        ТОГДА ВТВсеСкидки.СкидкаРодитель3
                    ИНАЧЕ ВЫБОР
                        КОГДА ВТВсеСкидки.СкидкаРодитель4 <> 0
                            ТОГДА ВТВсеСкидки.СкидкаРодитель4
                        ИНАЧЕ ВЫБОР
                            КОГДА ВТВсеСкидки.СкидкаРодитель5 <> 0
                                ТОГДА ВТВсеСкидки.СкидкаРодитель5
                            ИНАЧЕ ВЫБОР
                                КОГДА ВТВсеСкидки.СкидкаРодитель6 <> 0
                                    ТОГДА ВТВсеСкидки.СкидкаРодитель6
                                ИНАЧЕ ВЫБОР
                                    КОГДА ВТВсеСкидки.СкидкаРодитель7 <> 0
                                        ТОГДА ВТВсеСкидки.СкидкаРодитель7
                                    ИНАЧЕ 0
                                КОНЕЦ
                            КОНЕЦ
                        КОНЕЦ
                    КОНЕЦ
                КОНЕЦ
            КОНЕЦ
        КОНЕЦ
    КОНЕЦ КАК РезультирующаяСкидка
ИЗ
    ВТВсеСкидки КАК ВТВсеСкидки

Преимущества решения: выполняется за один запрос к СУБД, работает очень быстро на небольших базах.

Недостатки решения:

---

Решение 2. Динамическое построение текста запроса

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

Выясним максимальный уровень вложенности номенклатуры в базе данных с помощью метаданных (или сделав предварительный запрос к СУБД) и соберем текст запроса в коде:


МаксимальнаяГлубина = Метаданные.Справочники.Номенклатура.КоличествоУровней;

ТекстЗапросаПолей = "";
ТекстЗапросаСоединений = "";
ТекстВыбора = "ВЫБОР" + Символы.ПС;

Для Шаг = 1 По МаксимальнаяГлубина Цикл
    // Формируем путь к родителю
    ПутьКРодителю = "Номенклатура";
    Для Итератор = 1 По Шаг Цикл
        ПутьКРодителю = ПутьКРодителю + ".Родитель";
    КонецЦикла;
    
    ИмяСкидки = "СкидкаРодитель" + Строка(Шаг);
    
    ТекстЗапросаПолей = ТекстЗапросаПолей + ", ЕСТЬNULL(" + ИмяСкидки + ".Процент, 0) КАК " + ИмяСкидки + Символы.ПС;
    
    ТекстЗапросаСоединений = ТекстЗапросаСоединений + 
        " ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК " + ИмяСкидки + Символы.ПС +
        " ПО " + ПутьКРодителю + " = " + ИмяСкидки + ".ПолучательСкидки И " + ИмяСкидки + ".Контрагент = &Контрагент" + Символы.ПС;
        
    ТекстВыбора = ТекстВыбора + "	КОГДА ВТВсеСкидки." + ИмяСкидки + " <> 0 ТОГДА ВТВсеСкидки." + ИмяСкидки + Символы.ПС;
КонецЦикла;

ТекстВыбора = ТекстВыбора + "	ИНАЧЕ 0" + Символы.ПС + "КОНЕЦ";

// Сборка финального текста запроса...

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

---

Решение 3. Метод «Транзитивного замыкания» через временную таблицу

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

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

Разберем по шагам, как построить и использовать такую таблицу связей:

Шаг 1. Формирование плоской иерархии в запросе

Мы собираем итерационно все уровни связей в одну временную таблицу ВТ_СвязиИерархии:


// В цикле по уровням иерархии формируем пакетный запрос объединения
Запрос = Новый Запрос;
ТекстПакета = "";

МаксимальныйУровень = 10; // Определяем максимальный уровень вложенности в базе

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

// Помещаем результат во временную таблицу
Запрос.Текст = ТекстПакета + " ПОМЕСТИТЬ ВТ_СвязиИерархии";

В результате работы этого шага в таблице ВТ_СвязиИерархии для товара "Смартфон Х" появятся следующие строки:

Шаг 2. Соединение со скидками и поиск минимальной дистанции

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


// Продолжение пакета запросов...
ВЫБРАТЬ
    Связи.Товар КАК Товар,
    Связи.Родитель КАК Родитель,
    Связи.Дистанция КАК Дистанция,
    ЕСТЬNULL(Скидки.ПроцентСкидки, 0) КАК Скидка
ПОМЕСТИТЬ ВТ_СкидкиПоВсемУровням
ИЗ
    ВТ_СвязиИерархии КАК Связи
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК Скидки
        ПО Связи.Родитель = Скидки.ПолучательСкидки
        И Скидки.Контрагент = &Контрагент
;

////////////////////////////////////////////////////////////////////////////////
// Находим ближайший уровень (минимальную дистанцию), где скидка задана
ВЫБРАТЬ
    СкидкиПоУровням.Товар КАК Товар,
    МИНИМУМ(СкидкиПоУровням.Дистанция) КАК МинимальнаяДистанция
ПОМЕСТИТЬ ВТ_БлижайшиеСкидки
ИЗ
    ВТ_СкидкиПоВсемУровням КАК СкидкиПоУровням
ГРУППИРОВАТЬ ПО
    СкидкиПоУровням.Товар
;

////////////////////////////////////////////////////////////////////////////////
// Получаем итоговую скидку по найденному ближайшему родителю
ВЫБРАТЬ
    Ближайшие.Товар КАК Товар,
    СкидкиПоУровням.Скидка КАК Скидка
ИЗ
    ВТ_БлижайшиеСкидки КАК Ближайшие
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_СкидкиПоВсемУровням КАК СкидкиПоУровням
        ПО Ближайшие.Товар = СкидкиПоУровням.Товар
        И Ближайшие.МинимальнаяДистанция = СкидкиПоУровням.Дистанция

Почему это работает быстро? СУБД оперирует простыми плоскими таблицами во временной памяти, соединяя их по проиндексированным полям. Такой запрос выполняется за доли секунды даже для базы в десятки тысяч позиций номенклатуры.

---

Решение 4. Архитектурный подход: Переход на «Сегменты номенклатуры»

В современных конфигурациях 1С (ERP 2.x, Комплексная автоматизация, Управление торговлей 11) привязка скидок напрямую к физическим папкам справочника считается устаревшим антипаттерном. Физическая структура папок создается для удобства работы пользователей, и бизнес-логика расчетов не должна от нее зависеть. Если пользователь перенесет товар из одной папки в другую, скидка на него может неявно и ошибочно измениться.

Проанализируем правильную альтернативу — механизм Сегментов номенклатуры:

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

В этом случае запрос получения скидки упрощается до одного плоского соединения за один шаг:


ВЫБРАТЬ
    Номенклатура.Ссылка КАК Товар,
    ЕСТЬNULL(Скидки.ПроцентСкидки, 0) КАК Скидка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.НоменклатураВСегменте КАК СоставСегмента
        ПО Номенклатура.Ссылка = СоставСегмента.Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиПоСегментам КАК Скидки
        ПО СоставСегмента.Сегмент = Скидки.Сегмент
        И Скидки.Контрагент = &Контрагент

Этот способ обеспечивает высочайшую производительность СУБД (время выполнения — миллисекунды) и максимальную гибкость настройки скидок для бизнеса.

---

Решение 5. Использование ценовых групп

Если механизм сегментов кажется избыточным для вашей конфигурации, рассмотрите внедрение Ценовых групп номенклатуры. Это стандартный реквизит карточки товара, который связывает номенклатуру со схемой ценообразования напрямую:

Запрос получения скидки в данном случае тривиален и не требует обхода иерархических деревьев:


ВЫБРАТЬ
    Номенклатура.Ссылка КАК Товар,
    ЕСТЬNULL(Скидки.ПроцентСкидки, 0) КАК Скидка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиПоЦеновымГруппам КАК Скидки
        ПО Номенклатура.ЦеноваяГруппа = Скидки.ЦеноваяГруппа
        И Скидки.Контрагент = &Контрагент

---

Решение 6. Иерархические связи наборов данных в СКД

Если вашей финальной задачей является вывод отчета (например, Прайс-листа) через Систему компоновки данных (СКД), то задачу сопоставления иерархии можно переложить на саму платформу 1С.

Рассмотрим, как настроить такую связь в схеме компоновки:

  1. Создайте Набор данных 1 (Запрос), выбирающий список номенклатуры.
  2. Создайте Набор данных 2 (Запрос), выбирающий актуальные скидки из регистра сведений.
  3. Перейдите на вкладку Связи наборов данных и добавьте связь:
    • Источник связи: НаборДанных1 (поле Номенклатура)
    • Приемник связи: НаборДанных2 (поле ПапкаНоменклатуры)
    • Установите флаг Иерархическая связь.

Платформа 1С при формировании отчета СКД оптимизирует сопоставление данных по иерархическим связям автоматически на уровне внутренних механизмов работы с СУБД, что позволит сформировать прайс-лист быстро и без написания громоздкого кода.

---

Резюме и рекомендации

При выборе оптимального решения руководствуйтесь следующими правилами:

← На главную