При разработке сложных систем автоматизации на платформе 1С программисты часто сталкиваются с задачей распределения ресурсов, погашения партий или списания сумм по методу FIFO (First In, First Out). Традиционно такие задачи решаются обходом таблицы значений в цикле. Однако, когда объем данных растет (от 2 000 до 15 000 строк и выше), стандартный цикл может начать работать крайне медленно. В этой статье мы разберем, почему это происходит, как перенести расчет в запрос и какие подводные камни нас ожидают при работе с накопительными итогами в языке SQL.
Прежде чем переходить к запросам, давайте выясним причину низкой производительности циклов. Для 2 000 строк простой перебор в памяти должен занимать доли секунды. Если обработка "адски тормозит", скорее всего, внутри цикла выполняются "тяжелые" операции:
Строка.Номенклатура.СтавкаНДС) заставляет платформу обращаться к базе данных на каждой итерации.Объект.Записать() внутри цикла превращает одну транзакцию в тысячи маленьких, что катастрофически нагружает дисковую подсистему.ДокументОбъект или СправочникОбъект вместо Структура или ТаблицаЗначений создает лишнюю нагрузку на память.Если же бизнес-логика требует именно расчета накопительного итога (когда значение в текущей строке зависит от суммы всех предыдущих), то перенос этой логики в запрос может быть оправдан. Рассмотрим, как это реализовать.
Основная идея реализации накопительного итога в запросе 1С заключается в соединении таблицы самой с собой по условию "все строки, номер которых меньше или равен текущему". Рассмотрим этот метод на примере временной таблицы ВТ_Данные, где есть поле НомерСтроки и Значение.
Проанализируем следующий пример кода:
// Предположим, у нас есть временная таблица ВТ_Данные с полями:
// НомерСтроки (уникальный индекс), СуммаПогашения
ВЫБРАТЬ
Т1.НомерСтроки КАК НомерСтроки,
Т1.СуммаПогашения КАК ТекущаяСумма,
СУММА(Т2.СуммаПогашения) КАК НакопленныйИтог
ПОМЕСТИТЬ ВТ_СИтогом
ИЗ
ВТ_Данные КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Данные КАК Т2
ПО Т1.НомерСтроки >= Т2.НомерСтроки
СГРУППИРОВАТЬ ПО
Т1.НомерСтроки,
Т1.СуммаПогашения;
В данном запросе для каждой строки Т1 подтягиваются все строки Т2, у которых номер меньше или равен. Затем с помощью СУММА мы получаем желаемый результат. Важный нюанс: условие соединения должно строиться по строго уникальному полю. Если у вас нет уникального номера строки, можно использовать комбинацию даты, момента времени и ссылки документа.
Важно понимать математическую суть этого метода. Это алгоритм с вычислительной сложностью O(N²). Для 2 000 строк SQL-сервер выполнит примерно 2 миллиона сравнений. Для 15 000 строк — уже 225 миллионов. Именно поэтому на больших объемах (свыше 10 000 строк) запрос может начать уступать в скорости обычному циклу 1С, который работает линейно O(N).
Чтобы минимизировать тормоза в запросе, обязательно выполняйте следующие правила:
НомерСтроки), должно быть проиндексировано во временной таблице.Когда у нас есть накопленный итог, мы можем реализовать логику погашения. Допустим, у нас есть общая сумма Лимит, которую нужно распределить по строкам. Разберем, как это сделать в запросе:
ВЫБРАТЬ
Т.НомерСтроки,
Т.ТекущаяСумма,
Т.НакопленныйИтог,
ВЫБОР
КОГДА Т.НакопленныйИтог <= &Лимит ТОГДА Т.ТекущаяСумма
КОГДА Т.НакопленныйИтог - Т.ТекущаяСумма < &Лимит ТОГДА &Лимит - (Т.НакопленныйИтог - Т.ТекущаяСумма)
ИНАЧЕ 0
КОНЕЦ КАК РаспределенноеЗначение
ИЗ
ВТ_СИтогом КАК Т
В этом примере мы анализируем, попадает ли текущая строка в рамки лимита. Если накопленный итог вместе с текущей строкой меньше лимита — берем сумму полностью. Если лимит "закончился" на текущей строке — берем остаток. В остальных случаях — 0.
Малоизвестный, но крайне эффективный способ расчета итогов без "тяжелых" соединений — использование функций языка выражений СКД. Рассмотрим функцию ВычислитьВыражение. Она позволяет вычислять итоги в контексте группировок, работая при этом значительно быстрее, чем самосоединение таблиц в SQL.
Пример выражения для поля в СКД:
ВычислитьВыражение("Сумма(СуммаПогашения)", , "Группировка", "Первая", "Текущая")
Этот метод работает за один проход по выборке, обеспечивая линейную скорость выполнения даже на очень больших объемах данных.
Если вы решили оставить цикл, давайте сделаем его максимально быстрым. Главное правило: все данные должны быть подготовлены заранее. Проанализируем алгоритм:
ТаблицуЗначений.Для Каждого, выполняя только арифметические операции.Пример оптимизированного кода:
Лимит = 5000;
Накоплено = 0;
Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
ОстатокЛимита = Лимит - Накоплено;
Если ОстатокЛимита > 0 Тогда
ЗначениеДляЗаписи = Мин(СтрокаТЗ.Значение1, ОстатокЛимита);
СтрокаТЗ.Значение1 = ЗначениеДляЗаписи;
Накоплено = Накоплено + ЗначениеДляЗаписи;
Иначе
СтрокаТЗ.Значение1 = 0;
КонецЕсли;
КонецЦикла;
Такой код на 2 000 строках отработает мгновенно, так как нет обращений к базе данных внутри итераций.
Выясним, что же выбрать: запрос или цикл? Для небольших объемов (до 2 000 строк) правильно написанный цикл часто оказывается проще в поддержке и быстрее в разработке. Если же задача требует выполнения расчетов строго на стороне сервера БД или данные являются частью более крупного запроса, используйте левое соединение с обязательным индексированием временных таблиц. При работе с экстремальными объемами данных (десятки тысяч строк) рекомендуем смотреть в сторону СКД или механизмов прямого SQL (оконные функции OVER(ORDER BY)), если ваша конфигурация это позволяет.