При пакетной обработке сотен тысяч документов часто возникает проблема катастрофического падения производительности: операции, которые должны выполняться за пару часов, растягиваются на дни. Рассмотрим типовую ситуацию на примере алгоритма распределения взаиморасчетов по ФИФО и разберем по шагам ключевые узкие места в архитектуре и коде 1С.
Проанализируем исходный запрос к регистру остатков, выполняемый для каждого документа:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВзаиморасчетыОстатки.Документ,
| ВзаиморасчетыОстатки.СуммаОстаток,
| ВзаиморасчетыОстатки.Партнер,
| ВзаиморасчетыОстатки.Документ.Дата КАК ДокументДата
|ИЗ
| РегистрНакопления.Взаиморасчеты.Остатки(
| &Дата,
| Партнер = &Партнер
| И Документ <> &Пуст) КАК ВзаиморасчетыОстатки
|
|УПОРЯДОЧИТЬ ПО
| ДокументДата";
Запрос.УстановитьПараметр("Дата", Док.Ссылка.МоментВремени());
Запрос.УстановитьПараметр("Пуст", Неопределено);
Запрос.УстановитьПараметр("Партнер", Док.Партнер);
Выясним причину одной из самых частых «скрытых» просадок производительности. Если внутри цикла обработки документов вызываются интерактивные методы обновления интерфейса, такие как Состояние(), это провоцирует лавину клиент-серверных взаимодействий и перерисовок интерфейса:
// Ошибочный подход — вызов на каждой итерации
Состояние("Проводим " + Стр.Док, (тек * 100) / всего, "Номер док " + тек);
При 85 000 документов такой вызов отнимает до 90% всего времени работы процедуры. Если требуется визуализация прогресса, обновлять состояние следует не на каждом документе, а с определенным шагом (например, раз в 500–1000 итераций) или запускать тяжелую обработку через ФоновыеЗадания на стороне сервера без взаимодействия с клиентом.
Обратим внимание на конструкцию ВзаиморасчетыОстатки.Документ.Дата. Если измерение Документ имеет составной тип данных, платформа 1С преобразует обращение через точку в неявные левые соединения (LEFT OUTER JOIN) со всеми таблицами типов, входящих в составной тип. Это многократно утяжеляет план выполнения запроса в СУБД.
Для устранения этой проблемы применим следующие решения:
ДатаДокумента с типом Дата и включенной индексацией. Это позволит сортировать результат напрямую по полю регистра без разыменований.ВЫРАЗИТЬ: если составной тип неизбежен, но в конкретной ситуации тип документа известен, явно ограничим его: ВЫРАЗИТЬ(ВзаиморасчетыОстатки.Документ КАК Документ.РеализацияТоваровУслуг).Дата.Разберем порядок измерений регистра накопления и условия фильтрации:
Документ, а вторым Партнер, то фильтрация только по Партнер не может эффективно использовать индекс таблицы итогов. Для расчетов взаиморасчетов первым измерением должен выступать Партнер (или Договор), а вторым — расчетный Документ.Документ <> &Пуст лишает СУБД возможности выполнить точечный поиск по индексу (Index Seek), приводя к сканированию диапазона или всей таблицы. Для авансов и постоплат рекомендуется разделять учет по отдельным измерениям или флагам, чтобы фильтрация выполнялась только по строгому равенству (=).Док.Ссылка.МоментВремени(), Док.Партнер) вызывает дополнительные обращения к базе. Значения реквизитов следует получать заранее из выборки или структуры.Выполнение отдельного запроса к виртуальной таблице остатков на каждый документ многократно увеличивает накладные расходы. Рассмотрим алгоритм перевода на пакетную обработку:
МенеджерВременныхТаблиц.Граница:
ГраницаОстатков = Новый Граница(ТекущийМоментВремени, ВидГраницы.Исключая);
ТаблицаЗначений.Колонки.Индексы.Добавить()) или вычисляем распределение списания накопительным итогом прямо в теле запроса.Если логика распределения меняет движения только по одному конкретному регистру, не следует вызывать полное перепроведение документа через ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение). Полное проведение инициирует проверку всех модулей, блокировку всех связанных объектов и перезапись всех регистров документа.
Оптимальный подход — формировать и записывать только нужный набор записей регистра:
НаборЗаписей = РегистрыНакопления.Взаиморасчеты.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаДокумент);
// Заполняем движения набора согласно рассчитанному распределению ФИФО
Для Каждого Стр Из ТаблицаРаспределения Цикл
Движение = НаборЗаписей.Добавить();
Движение.Период = ДатаДокумента;
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Партнер = Партнер;
Движение.Документ = Стр.ДокументОснование;
Движение.Сумма = Стр.СуммаСписания;
КонецЦикла;
// Записываем только движения без вызова обработчиков проведения документа
НаборЗаписей.Записать();
Для стабильной работы алгоритма на больших объемах данных соблюдаем правила транзакционности:
НачатьТранзакцию() / ЗафиксироватьТранзакцию(), чтобы не переполнять журнал транзакций СУБД.БлокировкаДанных по измерению Партнер для предотвращения взаимных блокировок (дедлоков) при параллельной работе пользователей.