При разработке отчетов по взаиморасчетам программисты часто сталкиваются с задачей распределения платежей по документам отгрузки по методу ФИФО (первым пришел — первым ушел). Когда объемы данных небольшие, простая обработка таблиц значений в оперативной памяти работает быстро. Однако, когда количество строк в таблицах реализаций и оплат достигает 150 000 и более, стандартный перебор строк в цикле приводит к критической ошибке превышения доступной оперативной памяти (Out of Memory) — особенно на 32-битных серверах приложений 1С, где на одну сессию выделяется ограниченный объем ОЗУ.
В этой статье мы подробно разберем, почему возникает эта проблема, как оптимизировать работу с таблицами значений «на лету» и какие архитектурные подходы позволят обрабатывать сотни тысяч строк за секунды.
Рассмотрим типичный сценарий, который часто приводит к зависанию системы. На входе у нас есть две таблицы: таблица продаж (реализаций) и таблица оплат. Они связаны по аналитике: Организация, Контрагент, ДоговорКонтрагента, ТорговыйПредставитель. Прямых ссылочных связей между оплатами и продажами нет.
Обычно разработчики используют следующий алгоритм:
НайтиСтроки().Удалить(), чтобы уменьшить количество итераций на следующих шагах.Этот подход имеет две критические уязвимости:
НайтиСтроки() без индексации: Поиск выполняется перебором всей таблицы оплат для каждой строки продажи. При 150 000 строк в обеих таблицах количество операций стремится к $150 000 \times 150 000 = 22,5$ миллиардов итераций! Это классическая сложность $O(N^2)$.Если вам необходимо быстро исправить существующий код без глобального переписывания логики, мы можем применить два простых, но крайне эффективных приема: индексацию таблицы и использование флага обработки вместо удаления строк.
Проанализируем шаги оптимизации:
ТабОплат.Удалить()), мы добавим в нее вспомогательную колонку Обработан (тип Булево). При полном списании оплаты будем устанавливать этот флаг в значение Истина. Соответственно, это поле необходимо включить в структуру отбора и в индекс таблицы.Рассмотрим, как будет выглядеть оптимизированный код:
// Шаг 1. Подготовка структуры таблицы оплат и добавление индексов
ТабОплат.Колонки.Добавить("Обработан", Новый ОписаниеТипов("Булево"));
Для Каждого СтрОплаты Из ТабОплат Цикл
СтрОплаты.Обработан = Ложь;
КонецЦикла;
// Строим индекс по полям связи + флагу обработки
ТабОплат.Индексы.Добавить("Организация,Контрагент,ДоговорКонтрагента,ТорговыйПредставитель,Обработан");
// Шаг 2. Основной цикл распределения
Для Каждого СтрокаПродажи Из ТабПродажи Цикл
// Заполняем структуру отбора, включая неотработанные оплаты
ОтборОплат = Новый Структура;
ОтборОплат.Вставить("Организация", СтрокаПродажи.Организация);
ОтборОплат.Вставить("Контрагент", СтрокаПродажи.Контрагент);
ОтборОплат.Вставить("ДоговорКонтрагента", СтрокаПродажи.ДоговорКонтрагента);
ОтборОплат.Вставить("ТорговыйПредставитель", СтрокаПродажи.ТорговыйПредставитель);
ОтборОплат.Вставить("Обработан", Ложь);
// Благодаря индексу, поиск отработает мгновенно
СтрокиОплат = ТабОплат.НайтиСтроки(ОтборОплат);
СуммаПродажи = СтрокаПродажи.Реализация;
Для Каждого СтрОплата Из СтрокиОплат Цикл
Если СуммаПродажи = 0 Тогда
Прервать;
КонецЕсли;
СписатьДолг = Мин(СуммаПродажи, СтрОплата.Оплата);
// Корректируем остаток оплаты и долга
СтрОплата.Оплата = СтрОплата.Оплата - СписатьДолг;
СуммаПродажи = СуммаПродажи - СписатьДолг;
// Вместо удаления строки помечаем ее как обработанную
Если СтрОплата.Оплата = 0 Тогда
СтрОплата.Обработан = Истина;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Важное замечание по индексам: Платформа 1С автоматически пересчитывает индексы при изменении значений полей, входящих в этот индекс. Изменение значения СтрОплата.Обработан = Истина приведет к обновлению индекса «на лету», и при следующей итерации эта строка больше не попадет в выборку НайтиСтроки(). Это работает значительно быстрее, чем физическое удаление строк памяти.
Наиболее производительный способ сопоставления таблиц — перенос всех расчетов из оперативной памяти сервера приложений на сторону СУБД (SQL-сервера). Для этого применяется метод сопоставления интервалов нарастающего итога.
Выясним суть этого алгоритма по шагам:
ВЫБОР КОГДА.Для эффективного расчета нарастающего итога (начиная с платформы 1С 8.3.20) используются встроенные оконные функции. Они позволяют вычислить накопительную сумму за линейное время $O(N)$ без ресурсоемких соединений таблиц сами на себя.
Рассмотрим пример запроса, реализующего расчет шкал и их сопоставление:
// 1. Получаем продажи с накопительным итогом (шкала дебета)
ВЫБРАТЬ
Продажи.Организация,
Продажи.Контрагент,
Продажи.Договор,
Продажи.Документ,
Продажи.Сумма,
СУММА(Продажи.Сумма) ПО (РАЗБИТЬ ПО Продажи.Организация, Продажи.Контрагент, Продажи.Договор УПОРЯДОЧИТЬ ПО Продажи.Дата) КАК КонецИнтервалаДебета
ПОМЕСТИТЬ ВТ_ПродажиШкала
ИЗ
ВТ_ИсходныеПродажи КАК Продажи;
// 2. Получаем оплаты с накопительным итогом (шкала кредита)
ВЫБРАТЬ
Оплаты.Организация,
Оплаты.Контрагент,
Оплаты.Договор,
Оплаты.Документ,
Оплаты.Сумма,
СУММА(Оплаты.Сумма) ПО (РАЗБИТЬ ПО Оплаты.Организация, Оплаты.Контрагент, Оплаты.Договор УПОРЯДОЧИТЬ ПО Оплаты.Дата) КАК КонецИнтервалаКредита
ПОМЕСТИТЬ ВТ_ОплатыШкала
ИЗ
ВТ_ИсходныеОплаты КАК Оплаты;
// 3. Сопоставляем шкалы интервалов
ВЫБРАТЬ
Пр.Организация,
Пр.Контрагент,
Пр.Договор,
Пр.Документ КАК ДокументПродажи,
Оп.Документ КАК ДокументОплаты,
// Вычисляем сумму пересечения интервалов
ВЫБОР
КОГДА Пр.КонецИнтервалаДебета < Оп.КонецИнтервалаКредита ТОГДА Пр.КонецИнтервалаДебета
ИНАЧЕ Оп.КонецИнтервалаКредита
КОНЕЦ -
ВЫБОР
КОГДА (Пр.КонецИнтервалаДебета - Пр.Сумма) > (Оп.КонецИнтервалаКредита - Оп.Сумма) ТОГДА (Пр.КонецИнтервалаДебета - Пр.Сумма)
ИНАЧЕ (Оп.КонецИнтервалаКредита - Оп.Сумма)
КОНЕЦ КАК СуммаПокрытия
ИЗ
ВТ_ПродажиШкала КАК Пр
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ОплатыШкала КАК Оп
ПО Пр.Организация = Оп.Организация
И Пр.Контрагент = Оп.Контрагент
И Пр.Договор = Оп.Договор
// Условие пересечения интервалов
И (Пр.КонецИнтервалаДебета - Пр.Сумма) < Оп.КонецИнтервалаКредита
И Пр.КонецИнтервалаДебета > (Оп.КонецИнтервалаКредита - Оп.Сумма)
Этот запрос выполняется непосредственно в СУБД. На объемах в 150 000 строк SQL-сервер выполнит такое сопоставление за несколько секунд, вернув готовую детализированную таблицу ФИФО-распределения без малейшей нагрузки на оперативную память сервера приложений 1С.
Если вы ограничены старой версией платформы (где нет оконных функций) или бизнес-логика распределения настолько запутана, что ее невозможно полностью перевести на язык запросов, примените пакетную обработку.
Проанализируем стратегию разделения данных:
Вместо того чтобы загружать в память сразу все 150 000 строк по всем контрагентам, мы можем обрабатывать данные частями. Разделителем (пакетом) в данном случае логично выбрать ключевую аналитику — например, конкретного контрагента или группу контрагентов.
Разберем по шагам алгоритм пакетной обработки:
Очистить() для таблиц текущей итерации.Этот метод гарантирует, что потребление памяти сервером 1С будет стабильным и не превысит допустимые лимиты, независимо от общего объема базы данных.
Самый надежный и профессиональный способ решения проблем производительности при формировании тяжелых отчетов — это **отказ от динамического расчета "на лету"**. В типовых конфигурациях 1С (ERP, Комплексная автоматизация, Управление торговлей) расчет ФИФО никогда не выполняется в момент формирования отчета.
Рассмотрим, как устроена правильная архитектура взаиморасчетов:
РасчетыСКлиентамиПоДокументам).Если перед вами стоит задача долгосрочного развития системы, рекомендуется убедить руководство или заказчика перейти на регистрационную модель учета взаиморасчетов, так как любые динамические алгоритмы распределения рано или поздно упрутся в предел производительности оборудования при росте базы данных.