Как оптимизировать алгоритм ФИФО при сопоставлении больших таблиц реализаций и оплат в 1С?

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

При разработке отчетов по взаиморасчетам программисты часто сталкиваются с задачей распределения платежей по документам отгрузки по методу ФИФО (первым пришел — первым ушел). Когда объемы данных небольшие, простая обработка таблиц значений в оперативной памяти работает быстро. Однако, когда количество строк в таблицах реализаций и оплат достигает 150 000 и более, стандартный перебор строк в цикле приводит к критической ошибке превышения доступной оперативной памяти (Out of Memory) — особенно на 32-битных серверах приложений 1С, где на одну сессию выделяется ограниченный объем ОЗУ.

В этой статье мы подробно разберем, почему возникает эта проблема, как оптимизировать работу с таблицами значений «на лету» и какие архитектурные подходы позволят обрабатывать сотни тысяч строк за секунды.

Анализ неоптимального алгоритма

Рассмотрим типичный сценарий, который часто приводит к зависанию системы. На входе у нас есть две таблицы: таблица продаж (реализаций) и таблица оплат. Они связаны по аналитике: Организация, Контрагент, ДоговорКонтрагента, ТорговыйПредставитель. Прямых ссылочных связей между оплатами и продажами нет.

Обычно разработчики используют следующий алгоритм:

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

Этот подход имеет две критические уязвимости:


Решение 1. Быстрая оптимизация работы с таблицей значений

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

Проанализируем шаги оптимизации:

  1. Добавление индекса: Перед началом цикла обязательно добавим индекс по полям отбора в таблицу оплат. Это превратит линейный поиск в логарифмический, сократив время поиска в тысячи раз.
  2. Флаг вместо удаления: Вместо физического удаления строки из таблицы оплат (ТабОплат.Удалить()), мы добавим в нее вспомогательную колонку Обработан (тип Булево). При полном списании оплаты будем устанавливать этот флаг в значение Истина. Соответственно, это поле необходимо включить в структуру отбора и в индекс таблицы.

Рассмотрим, как будет выглядеть оптимизированный код:


// Шаг 1. Подготовка структуры таблицы оплат и добавление индексов
ТабОплат.Колонки.Добавить("Обработан", Новый ОписаниеТипов("Булево"));
Для Каждого СтрОплаты Из ТабОплат Цикл
    СтрОплаты.Обработан = Ложь;
КонецЦикла;

// Строим индекс по полям связи + флагу обработки
ТабОплат.Индексы.Добавить("Организация,Контрагент,ДоговорКонтрагента,ТорговыйПредставитель,Обработан");

// Шаг 2. Основной цикл распределения
Для Каждого СтрокаПродажи Из ТабПродажи Цикл
    
    // Заполняем структуру отбора, включая неотработанные оплаты
    ОтборОплат = Новый Структура;
    ОтборОплат.Вставить("Организация",          СтрокаПродажи.Организация);
    ОтборОплат.Вставить("Контрагент",           СтрокаПродажи.Контрагент);
    ОтборОплат.Вставить("ДоговорКонтрагента",   СтрокаПродажи.ДоговорКонтрагента);
    ОтборОплат.Вставить("ТорговыйПредставитель", СтрокаПродажи.ТорговыйПредставитель);
    ОтборОплат.Вставить("Обработан",            Ложь);
    
    // Благодаря индексу, поиск отработает мгновенно
    СтрокиОплат = ТабОплат.НайтиСтроки(ОтборОплат);
    
    СуммаПродажи = СтрокаПродажи.Реализация;
    
    Для Каждого СтрОплата Из СтрокиОплат Цикл
        Если СуммаПродажи = 0 Тогда
            Прервать;
        КонецЕсли;
        
        СписатьДолг = Мин(СуммаПродажи, СтрОплата.Оплата);
        
        // Корректируем остаток оплаты и долга
        СтрОплата.Оплата = СтрОплата.Оплата - СписатьДолг;
        СуммаПродажи     = СуммаПродажи - СписатьДолг;
        
        // Вместо удаления строки помечаем ее как обработанную
        Если СтрОплата.Оплата = 0 Тогда
            СтрОплата.Обработан = Истина;
        КонецЕсли;
    КонецЦикла;
    
КонецЦикла;

Важное замечание по индексам: Платформа 1С автоматически пересчитывает индексы при изменении значений полей, входящих в этот индекс. Изменение значения СтрОплата.Обработан = Истина приведет к обновлению индекса «на лету», и при следующей итерации эта строка больше не попадет в выборку НайтиСтроки(). Это работает значительно быстрее, чем физическое удаление строк памяти.


Решение 2. Расчет ФИФО в запросе через нарастающий итог

Наиболее производительный способ сопоставления таблиц — перенос всех расчетов из оперативной памяти сервера приложений на сторону СУБД (SQL-сервера). Для этого применяется метод сопоставления интервалов нарастающего итога.

Выясним суть этого алгоритма по шагам:

  1. Для каждой продажи рассчитывается «шкала долга». Например, первая продажа на 100 руб. образует интервал $[0, 100]$, вторая на 150 руб. — интервал $[100, 250]$.
  2. Для каждой оплаты аналогично рассчитывается «шкала платежей». Первый платеж на 120 руб. образует интервал $[0, 120]$, второй на 200 руб. — интервал $[120, 320]$.
  3. Две таблицы соединяются в запросе по условию пересечения интервалов: Граница начала оплаты должна быть меньше границы конца продажи, а граница конца оплаты — больше границы начала продажи.
  4. Сумма закрытия рассчитывается как пересечение этих отрезков с помощью математических конструкций ВЫБОР КОГДА.

Для эффективного расчета нарастающего итога (начиная с платформы 1С 8.3.20) используются встроенные оконные функции. Они позволяют вычислить накопительную сумму за линейное время $O(N)$ без ресурсоемких соединений таблиц сами на себя.

Рассмотрим пример запроса, реализующего расчет шкал и их сопоставление:


// 1. Получаем продажи с накопительным итогом (шкала дебета)
ВЫБРАТЬ
    Продажи.Организация,
    Продажи.Контрагент,
    Продажи.Договор,
    Продажи.Документ,
    Продажи.Сумма,
    СУММА(Продажи.Сумма) ПО (РАЗБИТЬ ПО Продажи.Организация, Продажи.Контрагент, Продажи.Договор УПОРЯДОЧИТЬ ПО Продажи.Дата) КАК КонецИнтервалаДебета
ПОМЕСТИТЬ ВТ_ПродажиШкала
ИЗ
    ВТ_ИсходныеПродажи КАК Продажи;

// 2. Получаем оплаты с накопительным итогом (шкала кредита)
ВЫБРАТЬ
    Оплаты.Организация,
    Оплаты.Контрагент,
    Оплаты.Договор,
    Оплаты.Документ,
    Оплаты.Сумма,
    СУММА(Оплаты.Сумма) ПО (РАЗБИТЬ ПО Оплаты.Организация, Оплаты.Контрагент, Оплаты.Договор УПОРЯДОЧИТЬ ПО Оплаты.Дата) КАК КонецИнтервалаКредита
ПОМЕСТИТЬ ВТ_ОплатыШкала
ИЗ
    ВТ_ИсходныеОплаты КАК Оплаты;

// 3. Сопоставляем шкалы интервалов
ВЫБРАТЬ
    Пр.Организация,
    Пр.Контрагент,
    Пр.Договор,
    Пр.Документ КАК ДокументПродажи,
    Оп.Документ КАК ДокументОплаты,
    // Вычисляем сумму пересечения интервалов
    ВЫБОР 
        КОГДА Пр.КонецИнтервалаДебета < Оп.КонецИнтервалаКредита ТОГДА Пр.КонецИнтервалаДебета 
        ИНАЧЕ Оп.КонецИнтервалаКредита 
    КОНЕЦ - 
    ВЫБОР 
        КОГДА (Пр.КонецИнтервалаДебета - Пр.Сумма) > (Оп.КонецИнтервалаКредита - Оп.Сумма) ТОГДА (Пр.КонецИнтервалаДебета - Пр.Сумма) 
        ИНАЧЕ (Оп.КонецИнтервалаКредита - Оп.Сумма) 
    КОНЕЦ КАК СуммаПокрытия
ИЗ
    ВТ_ПродажиШкала КАК Пр
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ОплатыШкала КАК Оп
        ПО Пр.Организация = Оп.Организация
        И Пр.Контрагент = Оп.Контрагент
        И Пр.Договор = Оп.Договор
        // Условие пересечения интервалов
        И (Пр.КонецИнтервалаДебета - Пр.Сумма) < Оп.КонецИнтервалаКредита
        И Пр.КонецИнтервалаДебета > (Оп.КонецИнтервалаКредита - Оп.Сумма)

Этот запрос выполняется непосредственно в СУБД. На объемах в 150 000 строк SQL-сервер выполнит такое сопоставление за несколько секунд, вернув готовую детализированную таблицу ФИФО-распределения без малейшей нагрузки на оперативную память сервера приложений 1С.


Решение 3. Порционная (пакетная) обработка данных (Chunking)

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

Проанализируем стратегию разделения данных:

Вместо того чтобы загружать в память сразу все 150 000 строк по всем контрагентам, мы можем обрабатывать данные частями. Разделителем (пакетом) в данном случае логично выбрать ключевую аналитику — например, конкретного контрагента или группу контрагентов.

Разберем по шагам алгоритм пакетной обработки:

  1. Получаем из базы данных список только уникальных контрагентов, у которых были движения за период.
  2. Разбиваем этот массив на небольшие порции (например, по 200-500 контрагентов).
  3. В цикле по порциям выбираем данные продаж и оплат только для текущего списка контрагентов. Объемы обрабатываемых таблиц в одной итерации снизятся со 150 000 до 1 000 – 3 000 строк.
  4. Применяем оптимизированный алгоритм сопоставления в памяти (Решение 1).
  5. Выгружаем полученный результат в промежуточный массив или временную таблицу на диске, после чего полностью очищаем оперативную память методом Очистить() для таблиц текущей итерации.

Этот метод гарантирует, что потребление памяти сервером 1С будет стабильным и не превысит допустимые лимиты, независимо от общего объема базы данных.


Решение 4. Архитектурный подход с использованием регистров

Самый надежный и профессиональный способ решения проблем производительности при формировании тяжелых отчетов — это **отказ от динамического расчета "на лету"**. В типовых конфигурациях 1С (ERP, Комплексная автоматизация, Управление торговлей) расчет ФИФО никогда не выполняется в момент формирования отчета.

Рассмотрим, как устроена правильная архитектура взаиморасчетов:

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

← На главную