При решении учетных и расчетных задач разработчики 1С часто сталкиваются с необходимостью распределения фиксированного количества или суммы по строкам таблицы значений (например, при реализации списания партий по методу FIFO, распределении оплат по накладным или резервировании остатков). Рассмотрим типовую ситуацию: имеется целевая сумма или количество X, а также таблица значений с перечнем позиций и их объемами. Наша цель — получить строки, сумма которых в точности равна X, усекая количество в граничной строке и отсекая все последующие записи.
Разберем по шагам, какими способами можно решить эту задачу на платформе 1С:Предприятие, проанализируем реализацию через программный обход коллекции и построение специализированного запроса с нарастающим итогом, а также оценим их производительность.
Если таблица значений уже находится в оперативной памяти, наиболее быстрым, надежным и экономичным с точки зрения ресурсов решением является линейный перебор строк в цикле. Алгоритмическая сложность такого подхода составляет O(N), где N — количество строк в исходной таблице.
Рассмотрим алгоритм решения:
X.Для Каждого.Прервать.Посмотрим на практический пример кода:
// Входные данные
ЦелевоеКоличество = 100;
ОстатокДляРаспределения = ЦелевоеКоличество;
ТЗ_Результат = ИсходнаяТЗ.СкопироватьКолонки();
Для Каждого СтрИсходная Из ИсходнаяТЗ Цикл
Если ОстатокДляРаспределения <= 0 Тогда
Прервать;
КонецЕсли;
СтрНовая = ТЗ_Результат.Добавить();
ЗаполнитьЗначенияСвойств(СтрНовая, СтрИсходная);
Если СтрИсходная.Количество <= ОстатокДляРаспределения Тогда
ОстатокДляРаспределения = ОстатокДляРаспределения - СтрИсходная.Количество;
Иначе
СтрНовая.Количество = ОстатокДляРаспределения;
ОстатокДляРаспределения = 0;
Прервать;
КонецЕсли;
КонецЦикла;
Также возможен вариант модификации исходной таблицы на месте без создания новой таблицы. Для этого можно обойти таблицу с конца (по индексу от Количество() - 1 до 0) или удалять лишние строки после достижения лимита. Однако создание новой таблицы способом выше чаще всего оказывается более прозрачным и безопасным.
Иногда задачу требуется решить непосредственно на уровне языка запросов 1С — например, при получении данных из базы или встраивании алгоритма в сложный пакетный запрос. Поскольку во встроенном языке запросов платформы отсутствуют классические оконные функции вида SUM() OVER(), нарастающий итог формируется с помощью самосоединения таблицы.
Разберем логику построения такого запроса:
НомерСтроки (или использовать детерминированное ключевое поле).Т1.НомерСтроки >= Т2.НомерСтроки. Это позволяет для каждой строки Т1 сопоставить все строки Т2, предшествующие ей.Т1, а количество суммируется по строкам Т2: СУММА(Т2.Количество) КАК НарастающийИтог.ГДЕ исключаются строки, нарастающая сумма до которых уже превысила лимит. С помощью конструкции ВЫБОР КОГДА вычисляется фактическое количество: если нарастающий итог превышает лимит, то берется разница между лимитом и суммой предыдущих строк.Рассмотрим пример кода запроса:
// Добавляем нумерацию для сохранения порядка
Если Не ИсходнаяТЗ.Колонки.Найти("НомерСтрокиПорядковый") = Неопределено Тогда
ИсходнаяТЗ.Колонки.Удалить("НомерСтрокиПорядковый");
КонецЕсли;
ИсходнаяТЗ.Колонки.Добавить("НомерСтрокиПорядковый", Новый ОписаниеТипов("Число"));
Для Индекс = 0 По ИсходнаяТЗ.Количество() - 1 Цикл
ИсходнаяТЗ[Индекс].НомерСтрокиПорядковый = Индекс + 1;
КонецЦикла;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Цвет КАК Цвет,
| ТЗ.Количество КАК Количество,
| ТЗ.НомерСтрокиПорядковый КАК НомерСтрокиПорядковый
|ПОМЕСТИТЬ ВТ_Исходная
|ИЗ
| &ТЗ КАК ТЗ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Т1.Цвет КАК Цвет,
| Т1.НомерСтрокиПорядковый КАК НомерСтрокиПорядковый,
| Т1.Количество КАК ИсходноеКоличество,
| СУММА(Т2.Количество) КАК НарастающийИтог
|ПОМЕСТИТЬ ВТ_НарастающийИтог
|ИЗ
| ВТ_Исходная КАК Т1
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Исходная КАК Т2
| ПО Т1.НомерСтрокиПорядковый >= Т2.НомерСтрокиПорядковый
|
|СГРУППИРОВАТЬ ПО
| Т1.НомерСтрокиПорядковый,
| Т1.Цвет,
| Т1.Количество
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Т.Цвет КАК Цвет,
| ВЫБОР
| КОГДА Т.НарастающийИтог <= &ЛимитX
| ТОГДА Т.ИсходноеКоличество
| ИНАЧЕ &ЛимитX - (Т.НарастающийИтог - Т.ИсходноеКоличество)
| КОНЕЦ КАК Количество
|ИЗ
| ВТ_НарастающийИтог КАК Т
|ГДЕ
| Т.НарастающийИтог - Т.ИсходноеКоличество < &ЛимитX
|
|УПОРЯДОЧИТЬ ПО
| Т.НомерСтрокиПорядковый";
Запрос.УстановитьПараметр("ТЗ", ИсходнаяТЗ);
Запрос.УстановитьПараметр("ЛимитX", 100);
РезультатТЗ = Запрос.Выполнить().Выгрузить();
Проанализируем технические ограничения и особенности каждого подхода:
1. Нагрузка на СУБД: При использовании самосоединения временной таблицы по неравенству сложность алгоритма составляет O(N²). Если в таблице 10–50 строк, запрос отработает мгновенно. Однако при нескольких сотнях или тысячах строк квадратичный рост декартова произведения создает колоссальную нагрузку на процессор и базу данных (в частности, на системную базу tempdb MS SQL Server). Линейный перебор через код 1С отрабатывает за O(N) без нагрузки на СУБД.
2. Накладные расходы передачи данных: Передача таблицы значений в запрос требует сериализации данных, создания временной таблицы в СУБД, ее индексации и последующей обратной выгрузки результата. Если данные уже получены в память, выполнять запрос нецелесообразно.
3. Непрерывность и неделимость: Описанные выше методы решают непрерывную задачу распределения (аналог списания FIFO), при которой последнюю позицию разрешено дробить. Если же по условиям задачи дробление запрещено (требуется подобрать комбинацию строк, дающую в сумме строго X без изменения количеств), то задача переходит в класс NP-полных задач («задача о рюкзаке» или Subset Sum Problem), которая решается алгоритмами динамического программирования или рекурсивным перебором.
Таким образом, для готовых таблиц значений оптимальным выбором является стандартный обход в цикле с вызовом Прервать, а расчет через запрос рекомендуется применять только тогда, когда данные изначально выбираются из таблиц базы данных и объединяются с остальной частью тяжелого запроса.