Как рассчитать накопительный итог и реализовать алгоритм FIFO в запросе 1С без использования циклов?

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

При разработке сложных систем автоматизации на платформе 1С программисты часто сталкиваются с задачей распределения ресурсов, погашения партий или списания сумм по методу FIFO (First In, First Out). Традиционно такие задачи решаются обходом таблицы значений в цикле. Однако, когда объем данных растет (от 2 000 до 15 000 строк и выше), стандартный цикл может начать работать крайне медленно. В этой статье мы разберем, почему это происходит, как перенести расчет в запрос и какие подводные камни нас ожидают при работе с накопительными итогами в языке SQL.

Анализируем проблему: почему цикл "тормозит"?

Прежде чем переходить к запросам, давайте выясним причину низкой производительности циклов. Для 2 000 строк простой перебор в памяти должен занимать доли секунды. Если обработка "адски тормозит", скорее всего, внутри цикла выполняются "тяжелые" операции:

  1. Запросы в цикле: Это самая частая ошибка. Получение остатков по регистру или обращение к реквизитам через точку (например, Строка.Номенклатура.СтавкаНДС) заставляет платформу обращаться к базе данных на каждой итерации.
  2. Запись объектов: Вызов метода Объект.Записать() внутри цикла превращает одну транзакцию в тысячи маленьких, что катастрофически нагружает дисковую подсистему.
  3. Работа с тяжелыми объектами: Использование ДокументОбъект или СправочникОбъект вместо Структура или ТаблицаЗначений создает лишнюю нагрузку на память.

Если же бизнес-логика требует именно расчета накопительного итога (когда значение в текущей строке зависит от суммы всех предыдущих), то перенос этой логики в запрос может быть оправдан. Рассмотрим, как это реализовать.

Классический метод: Накопительный итог через левое соединение

Основная идея реализации накопительного итога в запросе 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).

Чтобы минимизировать тормоза в запросе, обязательно выполняйте следующие правила:

  1. Индексация: Поле, по которому идет соединение (НомерСтроки), должно быть проиндексировано во временной таблице.
  2. Минимизация данных: Не передавайте в таблицу для расчета лишние поля. Только идентификатор строки и числовое значение.
  3. Разделение пакетов: Сначала рассчитайте итог, поместите его во временную таблицу, и только потом соединяйте с основными данными.

Реализация алгоритма FIFO (погашение остатка)

Когда у нас есть накопленный итог, мы можем реализовать логику погашения. Допустим, у нас есть общая сумма Лимит, которую нужно распределить по строкам. Разберем, как это сделать в запросе:


ВЫБРАТЬ
    Т.НомерСтроки,
    Т.ТекущаяСумма,
    Т.НакопленныйИтог,
    ВЫБОР
        КОГДА Т.НакопленныйИтог <= &Лимит ТОГДА Т.ТекущаяСумма
        КОГДА Т.НакопленныйИтог - Т.ТекущаяСумма < &Лимит ТОГДА &Лимит - (Т.НакопленныйИтог - Т.ТекущаяСумма)
        ИНАЧЕ 0
    КОНЕЦ КАК РаспределенноеЗначение
ИЗ
    ВТ_СИтогом КАК Т

В этом примере мы анализируем, попадает ли текущая строка в рамки лимита. Если накопленный итог вместе с текущей строкой меньше лимита — берем сумму полностью. Если лимит "закончился" на текущей строке — берем остаток. В остальных случаях — 0.

Альтернатива 1: Использование СКД (Система компоновки данных)

Малоизвестный, но крайне эффективный способ расчета итогов без "тяжелых" соединений — использование функций языка выражений СКД. Рассмотрим функцию ВычислитьВыражение. Она позволяет вычислять итоги в контексте группировок, работая при этом значительно быстрее, чем самосоединение таблиц в SQL.

Пример выражения для поля в СКД:

ВычислитьВыражение("Сумма(СуммаПогашения)", , "Группировка", "Первая", "Текущая")

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

Альтернатива 2: Правильный цикл в памяти

Если вы решили оставить цикл, давайте сделаем его максимально быстрым. Главное правило: все данные должны быть подготовлены заранее. Проанализируем алгоритм:

  1. Одним запросом выгружаем все необходимые данные в ТаблицуЗначений.
  2. Индексируем таблицу в памяти, если нужны дополнительные поиски.
  3. Обходим таблицу в цикле Для Каждого, выполняя только арифметические операции.
  4. После завершения цикла одним пакетом обновляем данные в базе или записываем документ.

Пример оптимизированного кода:


Лимит = 5000;
Накоплено = 0;
Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
    ОстатокЛимита = Лимит - Накоплено;
    Если ОстатокЛимита > 0 Тогда
        ЗначениеДляЗаписи = Мин(СтрокаТЗ.Значение1, ОстатокЛимита);
        СтрокаТЗ.Значение1 = ЗначениеДляЗаписи;
        Накоплено = Накоплено + ЗначениеДляЗаписи;
    Иначе
        СтрокаТЗ.Значение1 = 0;
    КонецЕсли;
КонецЦикла;

Такой код на 2 000 строках отработает мгновенно, так как нет обращений к базе данных внутри итераций.

Подведение итогов

Выясним, что же выбрать: запрос или цикл? Для небольших объемов (до 2 000 строк) правильно написанный цикл часто оказывается проще в поддержке и быстрее в разработке. Если же задача требует выполнения расчетов строго на стороне сервера БД или данные являются частью более крупного запроса, используйте левое соединение с обязательным индексированием временных таблиц. При работе с экстремальными объемами данных (десятки тысяч строк) рекомендуем смотреть в сторону СКД или механизмов прямого SQL (оконные функции OVER(ORDER BY)), если ваша конфигурация это позволяет.

← На главную