В практике автоматизации складской логистики мы часто сталкиваемся с задачами распределения запасов. Одна из классических постановок звучит так: у нас есть определенное количество товара в зоне приемки (или в "Помещении 1"), и нам необходимо распределить этот товар по ячейкам хранения (Помещениям 2, 3 и т.д.), каждая из которых имеет свою максимальную вместимость. Как реализовать этот алгоритм, если мы хотим получить результат непосредственно в выборке данных?
Давайте подробно разберем эту задачу, проанализируем ограничения платформы 1С:Предприятие и найдем наиболее эффективные способы решения. Мы рассмотрим варианты реализации как на языке запросов, так и средствами встроенного языка.
Давайте формализуем условия, с которыми нам предстоит работать. У нас есть входящий поток товаров и набор мест хранения с ограничениями.
Рассмотрим пример, приведенный в обсуждении:
Ситуация может усложняться (пример 2):
С алгоритмической точки зрения это задача аллокации (распределения) потребностей, часто называемая "закрытием по FIFO" или "расчетом нарастающего итога". Главный вопрос, который возникает у разработчиков: можно ли это сделать одним изящным запросом?
Многие разработчики, знакомые с SQL, знают о существовании циклов и переменных в T-SQL или PL/SQL. Однако, когда мы работаем с языком запросов 1С, мы должны помнить о его ключевом ограничении: в нем отсутствуют циклы и переменные, которые можно изменять в процессе выполнения запроса.
Чтобы реализовать логику "взять 200, положить 100, остаток 100 перенести к следующему шагу", нам необходим расчет нарастающего итога. В языке запросов 1С это реализуется через соединение таблицы самой с собой (Self-Join), что порождает проблему производительности.
Давайте посмотрим, как это выглядит концептуально. Чтобы посчитать, сколько товара достанется "Помещению 3", нам нужно знать, сколько уже забрало "Помещение 2". Если помещений много, сложность алгоритма растет квадратично (O(N^2)).
Если у вас 10 помещений, база данных сделает около 55 сравнений. Если же помещений будет 2000, количество операций превысит 2 миллиона. Как справедливо заметили участники обсуждения, такой подход может серьезно замедлить работу системы.
Несмотря на неэффективность, давайте разберем, как технически это реализуется, чтобы понимать механику "треугольного соединения". Это полезно для небольших наборов данных.
Представим, что у нас есть таблица потребностей (куда надо положить товар) с приоритетами.
ВЫБРАТЬ
Потребности.Помещение КАК Помещение,
Потребности.Вместимость КАК Вместимость,
Потребности.Приоритет КАК Приоритет
ПОМЕСТИТЬ ВТ_Потребности
ИЗ
&ТЗПотребности КАК Потребности
;
Теперь, чтобы понять накопительный итог вместимости, мы соединяем эту таблицу с самой собой:
ВЫБРАТЬ
Т1.Помещение,
Т1.Вместимость,
СУММА(Т2.Вместимость) КАК НарастающаяВместимость
ПОМЕСТИТЬ ВТ_Нарастающие
ИЗ
ВТ_Потребности КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Потребности КАК Т2
ПО Т1.Приоритет >= Т2.Приоритет
СГРУППИРОВАТЬ ПО
Т1.Помещение,
Т1.Вместимость
Теперь у нас есть поле НарастающаяВместимость. Если наш общий остаток товара равен 150, мы можем использовать конструкцию ВЫБОР для определения количества к распределению для каждой строки:
ВЫБОР
// Если нарастающая потребность меньше остатка, берем всю емкость текущего помещения
КОГДА НарастающаяВместимость <= &ОбщийОстатокТовара ТОГДА Вместимость
// Если предыдущая нарастающая (Нарастающая - Текущая) уже больше остатка, то 0
КОГДА (НарастающаяВместимость - Вместимость) >= &ОбщийОстатокТовара ТОГДА 0
// Иначе берем хвостик
ИНАЧЕ &ОбщийОстатокТовара - (НарастающаяВместимость - Вместимость)
КОНЕЦ КАК Распределено
Важно: Этот метод работает приемлемо только на малых выборках. На реальных объемах склада (тысячи ячеек) он "положит" производительность.
Более продвинутый способ, предложенный в обсуждении, — использование возможностей СКД. Система компоновки данных имеет собственный язык выражений, который выполняется уже после получения данных из базы, но гораздо быстрее, чем запрос с само-соединением.
В СКД существует функция ВычислитьВыражение, которая позволяет обращаться к другим строкам группировки. Мы можем использовать её для расчета нарастающего итога.
Пример выражения ресурса в СКД для расчета накопленной суммы:
ВычислитьВыражение("Сумма(Вместимость)", , , "Первая", "Текущая")
Используя эту конструкцию, мы получаем аналог НарастающаяВместимость из предыдущего примера, но без тяжелого соединения таблиц в SQL. Далее, используя условное оформление или вычисляемые поля, мы можем применить ту же логику распределения остатка.
Плюсы СКД:
Самый надежный, понятный и производительный способ решения задачи распределения товаров — это использование встроенного языка 1С и таблиц значений. Алгоритмическая сложность такого решения — линейная (O(N)), что означает мгновенное выполнение даже для тысяч записей.
Давайте напишем алгоритм, который решает задачу автора темы.
Сначала получим данные запросом. Нам нужны две таблицы: Товары (что распределяем) и Помещения (куда распределяем, отсортированные по приоритету).
// Получаем остатки товаров
Запрос.Текст = "ВЫБРАТЬ Товар, Количество ОСТАТКИ ...";
ТЗ_Товары = Запрос.Выполнить().Выгрузить();
// Получаем свободные ячейки, ОБЯЗАТЕЛЬНО упорядочиваем их
Запрос.Текст = "ВЫБРАТЬ Ячейка, Вместимость ИЗ ... УПОРЯДОЧИТЬ ПО ПорядокОбхода";
ТЗ_Ячейки = Запрос.Выполнить().Выгрузить();
Теперь реализуем логику "жадного" алгоритма. Мы берем товар и пытаемся "влить" его в доступные ячейки по очереди.
ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Товар");
ТаблицаРезультат.Колонки.Добавить("Ячейка");
ТаблицаРезультат.Колонки.Добавить("Количество");
Для Каждого СтрТовар Из ТЗ_Товары Цикл
ОстатокКРаспределению = СтрТовар.Количество;
// Бежим по ячейкам, пока есть товар
Для Каждого СтрЯчейка Из ТЗ_Ячейки Цикл
Если ОстатокКРаспределению <= 0 Тогда
Прервать; // Товар кончился
КонецЕсли;
// Если ячейка уже заполнена (в данном примере считаем,
// что поле Вместимость - это свободное место), пропускаем
Если СтрЯчейка.Вместимость <= 0 Тогда
Продолжить;
КонецЕсли;
// Сколько можем положить в эту ячейку?
// Либо всё что есть, либо сколько влезает
КоличествоКПеремещению = Мин(ОстатокКРаспределению, СтрЯчейка.Вместимость);
// Записываем результат
НовСтр = ТаблицаРезультат.Добавить();
НовСтр.Товар = СтрТовар.Товар;
НовСтр.Ячейка = СтрЯчейка.Ячейка;
НовСтр.Количество = КоличествоКПеремещению;
// Уменьшаем остаток товара на руках
ОстатокКРаспределению = ОстатокКРаспределению - КоличествоКПеремещению;
// Уменьшаем свободное место в ячейке (важно, если ячейка используется для разных товаров)
СтрЯчейка.Вместимость = СтрЯчейка.Вместимость - КоличествоКПеремещению;
КонецЦикла;
КонецЦикла;
В вопросе автора упоминается ситуация, когда источников несколько (Помещения 4 и 5) и приемник один (Помещение 6). Наш алгоритм выше легко адаптируется и под это.
Если нам нужно переместить товары ИЗ нескольких мест В несколько мест, мы можем создать общую таблицу "Потребностей" (откуда взять) и таблицу "Предложений" (куда положить). Однако чаще всего на практике это делается в два этапа:
Подведем итоги. Если перед вами стоит задача распределения количества с учетом лимитов: