При разработке торговых систем на платформе 1С:Предприятие одной из классических задач является расчет автоматических скидок. Часто скидки назначаются не на каждый конкретный товар, а на целые группы (папки) номенклатуры. При этом логика поиска скидки в документе или отчете выглядит следующим образом: система ищет скидку непосредственно для самого товара; если её нет, обращается к его родителю; если нет у родителя — к родителю родителя, и так далее до самого верха иерархии.
Если для одного документа такая логика, реализованная через цикл в общем модуле, работает приемлемо, то при попытке построить отчет (например, «Прайс-лист со скидками для клиента») на 20 000 позиций система сталкивается с критическим падением производительности. Вызов функции общего модуля в цикле для каждой из 20 000 строк порождает десятки тысяч запросов к базе данных, что приводит к зависанию системы. Создание вспомогательного регистра с предрассчитанными скидками (для связок «20 000 товаров * 7 000 клиентов = 140 миллионов записей») также не всегда жизнеспособно из-за колоссального объема данных и нагрузки на регламентные задания.
В этой статье мы подробно разберем, как решить эту проблему, проанализируем различные методы оптимизации — от простых запросов до изменения архитектуры хранения данных.
---
Рассмотрим причину низкой производительности классического итерационного обхода. Когда мы для каждого товара вызываем код, выполняющий поиск по иерархии вверх:
SELECT вместо одного пакетного запроса, который обрабатывает все данные за один проход.Наша цель — перенести всю логику поиска «ближайшего родителя со скидкой» на уровень СУБД и выполнить расчет за один единый запрос.
---
Это самое быстрое в реализации решение, которое отлично подходит, если максимальная глубина иерархии в вашем справочнике ограничена и заранее известна (например, не превышает 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
КОНЕЦ
КОНЕЦ
КОНЕЦ
КОНЕЦ
КОНЕЦ
КОНЕЦ
КОНЕЦ
КОНЕЦ КАК РезультирующаяСкидка
ИЗ
ВТВсеСкидки КАК ВТВсеСкидки
Преимущества решения: выполняется за один запрос к СУБД, работает очень быстро на небольших базах.
Недостатки решения:
Номенклатура.Родитель.Родитель... платформа генерирует неявные левые соединения (Implicit Left Joins) на уровне SQL. Большое количество таких соединений может усложнить план запроса СУБД и замедлить выполнение на больших таблицах.---
Чтобы обойти ограничение жестко зашитых уровней вложенности, мы можем определять максимальную глубину структуры каталога динамически перед выполнением отчета. Для этого программно сгенерируем текст конструкции ВЫБОР КОГДА.
Выясним максимальный уровень вложенности номенклатуры в базе данных с помощью метаданных (или сделав предварительный запрос к СУБД) и соберем текст запроса в коде:
МаксимальнаяГлубина = Метаданные.Справочники.Номенклатура.КоличествоУровней;
ТекстЗапросаПолей = "";
ТекстЗапросаСоединений = "";
ТекстВыбора = "ВЫБОР" + Символы.ПС;
Для Шаг = 1 По МаксимальнаяГлубина Цикл
// Формируем путь к родителю
ПутьКРодителю = "Номенклатура";
Для Итератор = 1 По Шаг Цикл
ПутьКРодителю = ПутьКРодителю + ".Родитель";
КонецЦикла;
ИмяСкидки = "СкидкаРодитель" + Строка(Шаг);
ТекстЗапросаПолей = ТекстЗапросаПолей + ", ЕСТЬNULL(" + ИмяСкидки + ".Процент, 0) КАК " + ИмяСкидки + Символы.ПС;
ТекстЗапросаСоединений = ТекстЗапросаСоединений +
" ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК " + ИмяСкидки + Символы.ПС +
" ПО " + ПутьКРодителю + " = " + ИмяСкидки + ".ПолучательСкидки И " + ИмяСкидки + ".Контрагент = &Контрагент" + Символы.ПС;
ТекстВыбора = ТекстВыбора + " КОГДА ВТВсеСкидки." + ИмяСкидки + " <> 0 ТОГДА ВТВсеСкидки." + ИмяСкидки + Символы.ПС;
КонецЦикла;
ТекстВыбора = ТекстВыбора + " ИНАЧЕ 0" + Символы.ПС + "КОНЕЦ";
// Сборка финального текста запроса...
Этот подход делает код универсальным и масштабируемым под любые изменения иерархии номенклатуры, однако он по-прежнему генерирует большое количество левых соединений на уровне SQL.
---
Это наиболее профессиональный и быстрый алгоритм с точки зрения СУБД. Он позволяет полностью избавиться от неявных левых соединений через точку и найти ближайшего родителя на любой глубине вложенности.
Суть алгоритма: мы строим плоскую таблицу связей каждого товара со всеми его родителями (включая самого себя) и указываем дистанцию (количество шагов вверх по иерархии). Затем соединяем эту таблицу с регистром скидок и выбираем строку с минимальной дистанцией.
Разберем по шагам, как построить и использовать такую таблицу связей:
Мы собираем итерационно все уровни связей в одну временную таблицу ВТ_СвязиИерархии:
// В цикле по уровням иерархии формируем пакетный запрос объединения
Запрос = Новый Запрос;
ТекстПакета = "";
МаксимальныйУровень = 10; // Определяем максимальный уровень вложенности в базе
Для Уровень = 0 По МаксимальныйУровень Цикл
Если Уровень > 0 Тогда
ТекстПакета = ТекстПакета + " ОБЪЕДИНИТЬ ВСЕ " + Символы.ПС;
КонецЕсли;
ПутьКРодителю = "Ссылка";
Для Шаг = 1 По Уровень Цикл
ПутьКРодителю = ПутьКРодителю + ".Родитель";
КонецЦикла;
ТекстПакета = ТекстПакета +
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Товар,
| Номенклатура." + ПутьКРодителю + " КАК Родитель,
| " + Строка(Уровень) + " КАК Дистанция
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
| И Номенклатура." + ПутьКРодителю + " <> ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)";
КонецЦикла;
// Помещаем результат во временную таблицу
Запрос.Текст = ТекстПакета + " ПОМЕСТИТЬ ВТ_СвязиИерархии";
В результате работы этого шага в таблице ВТ_СвязиИерархии для товара "Смартфон Х" появятся следующие строки:
Добавим в пакет запроса финальную выборку. Мы соединим связи с регистром скидок, найдем для каждого товара минимальную дистанцию, на которой нашлась скидка, и получим значение этой скидки:
// Продолжение пакета запросов...
ВЫБРАТЬ
Связи.Товар КАК Товар,
Связи.Родитель КАК Родитель,
Связи.Дистанция КАК Дистанция,
ЕСТЬNULL(Скидки.ПроцентСкидки, 0) КАК Скидка
ПОМЕСТИТЬ ВТ_СкидкиПоВсемУровням
ИЗ
ВТ_СвязиИерархии КАК Связи
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиНоменклатуры КАК Скидки
ПО Связи.Родитель = Скидки.ПолучательСкидки
И Скидки.Контрагент = &Контрагент
;
////////////////////////////////////////////////////////////////////////////////
// Находим ближайший уровень (минимальную дистанцию), где скидка задана
ВЫБРАТЬ
СкидкиПоУровням.Товар КАК Товар,
МИНИМУМ(СкидкиПоУровням.Дистанция) КАК МинимальнаяДистанция
ПОМЕСТИТЬ ВТ_БлижайшиеСкидки
ИЗ
ВТ_СкидкиПоВсемУровням КАК СкидкиПоУровням
ГРУППИРОВАТЬ ПО
СкидкиПоУровням.Товар
;
////////////////////////////////////////////////////////////////////////////////
// Получаем итоговую скидку по найденному ближайшему родителю
ВЫБРАТЬ
Ближайшие.Товар КАК Товар,
СкидкиПоУровням.Скидка КАК Скидка
ИЗ
ВТ_БлижайшиеСкидки КАК Ближайшие
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_СкидкиПоВсемУровням КАК СкидкиПоУровням
ПО Ближайшие.Товар = СкидкиПоУровням.Товар
И Ближайшие.МинимальнаяДистанция = СкидкиПоУровням.Дистанция
Почему это работает быстро? СУБД оперирует простыми плоскими таблицами во временной памяти, соединяя их по проиндексированным полям. Такой запрос выполняется за доли секунды даже для базы в десятки тысяч позиций номенклатуры.
---
В современных конфигурациях 1С (ERP 2.x, Комплексная автоматизация, Управление торговлей 11) привязка скидок напрямую к физическим папкам справочника считается устаревшим антипаттерном. Физическая структура папок создается для удобства работы пользователей, и бизнес-логика расчетов не должна от нее зависеть. Если пользователь перенесет товар из одной папки в другую, скидка на него может неявно и ошибочно измениться.
Проанализируем правильную альтернативу — механизм Сегментов номенклатуры:
СегментыНоменклатуры. Сегмент может формироваться как вручную, так и динамически по правилам (например, по конкретным свойствам, производителю или ценовому диапазону).НоменклатураВСегменте (измерения: Сегмент, Номенклатура). При записи товара или по регламентному заданию этот регистр перезаполняется.В этом случае запрос получения скидки упрощается до одного плоского соединения за один шаг:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Товар,
ЕСТЬNULL(Скидки.ПроцентСкидки, 0) КАК Скидка
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.НоменклатураВСегменте КАК СоставСегмента
ПО Номенклатура.Ссылка = СоставСегмента.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиПоСегментам КАК Скидки
ПО СоставСегмента.Сегмент = Скидки.Сегмент
И Скидки.Контрагент = &Контрагент
Этот способ обеспечивает высочайшую производительность СУБД (время выполнения — миллисекунды) и максимальную гибкость настройки скидок для бизнеса.
---
Если механизм сегментов кажется избыточным для вашей конфигурации, рассмотрите внедрение Ценовых групп номенклатуры. Это стандартный реквизит карточки товара, который связывает номенклатуру со схемой ценообразования напрямую:
ЦеноваяГруппа (без иерархии).Запрос получения скидки в данном случае тривиален и не требует обхода иерархических деревьев:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Товар,
ЕСТЬNULL(Скидки.ПроцентСкидки, 0) КАК Скидка
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкидкиПоЦеновымГруппам КАК Скидки
ПО Номенклатура.ЦеноваяГруппа = Скидки.ЦеноваяГруппа
И Скидки.Контрагент = &Контрагент
---
Если вашей финальной задачей является вывод отчета (например, Прайс-листа) через Систему компоновки данных (СКД), то задачу сопоставления иерархии можно переложить на саму платформу 1С.
Рассмотрим, как настроить такую связь в схеме компоновки:
Номенклатура)ПапкаНоменклатуры)Платформа 1С при формировании отчета СКД оптимизирует сопоставление данных по иерархическим связям автоматически на уровне внутренних механизмов работы с СУБД, что позволит сформировать прайс-лист быстро и без написания громоздкого кода.
---
При выборе оптимального решения руководствуйтесь следующими правилами: