Как отобрать строки таблицы значений на заданную сумму с ограничением?

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

При решении учетных и расчетных задач разработчики 1С часто сталкиваются с необходимостью распределения фиксированного количества или суммы по строкам таблицы значений (например, при реализации списания партий по методу FIFO, распределении оплат по накладным или резервировании остатков). Рассмотрим типовую ситуацию: имеется целевая сумма или количество X, а также таблица значений с перечнем позиций и их объемами. Наша цель — получить строки, сумма которых в точности равна X, усекая количество в граничной строке и отсекая все последующие записи.

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

Способ 1. Программный обход в цикле (линейный алгоритм)

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

Рассмотрим алгоритм решения:

  1. Создаем копию структуры таблицы для записи результата или инициализируем переменную накапливаемого остатка целевого значения X.
  2. Обходим строки исходной таблицы значений циклом Для Каждого.
  3. Сравниваем количество в текущей строке с оставшейся нераспределенной суммой.
  4. Если количество в строке меньше или равно остатку, добавляем строку целиком и уменьшаем остаток.
  5. Если количество строки превышает остаток, корректируем значение текущей строки до величины остатка, обнуляем его и завершаем выполнение цикла с помощью оператора Прервать.

Посмотрим на практический пример кода:


// Входные данные
ЦелевоеКоличество = 100;
ОстатокДляРаспределения = ЦелевоеКоличество;

ТЗ_Результат = ИсходнаяТЗ.СкопироватьКолонки();

Для Каждого СтрИсходная Из ИсходнаяТЗ Цикл
    Если ОстатокДляРаспределения <= 0 Тогда
        Прервать;
    КонецЕсли;
    
    СтрНовая = ТЗ_Результат.Добавить();
    ЗаполнитьЗначенияСвойств(СтрНовая, СтрИсходная);
    
    Если СтрИсходная.Количество <= ОстатокДляРаспределения Тогда
        ОстатокДляРаспределения = ОстатокДляРаспределения - СтрИсходная.Количество;
    Иначе
        СтрНовая.Количество = ОстатокДляРаспределения;
        ОстатокДляРаспределения = 0;
        Прервать;
    КонецЕсли;
КонецЦикла;

Также возможен вариант модификации исходной таблицы на месте без создания новой таблицы. Для этого можно обойти таблицу с конца (по индексу от Количество() - 1 до 0) или удалять лишние строки после достижения лимита. Однако создание новой таблицы способом выше чаще всего оказывается более прозрачным и безопасным.

Способ 2. Реализация через запрос (нарастающий итог)

Иногда задачу требуется решить непосредственно на уровне языка запросов 1С — например, при получении данных из базы или встраивании алгоритма в сложный пакетный запрос. Поскольку во встроенном языке запросов платформы отсутствуют классические оконные функции вида SUM() OVER(), нарастающий итог формируется с помощью самосоединения таблицы.

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

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

Рассмотрим пример кода запроса:


// Добавляем нумерацию для сохранения порядка
Если Не ИсходнаяТЗ.Колонки.Найти("НомерСтрокиПорядковый") = Неопределено Тогда
    ИсходнаяТЗ.Колонки.Удалить("НомерСтрокиПорядковый");
КонецЕсли;
ИсходнаяТЗ.Колонки.Добавить("НомерСтрокиПорядковый", Новый ОписаниеТипов("Число"));

Для Индекс = 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), которая решается алгоритмами динамического программирования или рекурсивным перебором.

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

← На главную