При программной обработке или перезаписи движений документов, формирующих тысячи проводок (например, регламентный документ ЗакрытиеМесяца), разработчики нередко сталкиваются с катастрофическим падением производительности. Ситуация, когда запись набора из 9 000–10 000 строк занимает несколько часов, а замер производительности показывает, что 99% времени уходит на метод НаборЗаписей.Записать(), встречается на практике довольно часто. Разберем причины такого поведения платформы и СУБД, а также детально рассмотрим способы оптимизации этой операции.
Проанализируем внутреннее устройство регистра бухгалтерии с поддержкой субконто. В отличие от простых регистров сведений или накопления, регистр бухгалтерии физически состоит из множества связанных таблиц:
Когда мы вызываем метод Записать() для набора из 10 000 проводок, платформа не просто вставляет строки в одну таблицу. СУБД выполняет сотни тысяч вставок и обновлений связанных индексов. Более того, если дата документа находится в прошлом периоде, платформа 1С инициирует автоматический каскадный пересчет ежемесячных срезов итогов от даты документа до текущего момента времени.
В среде СУБД PostgreSQL проблема усугубляется механизмом очистки: при стандартной перезаписи платформа сначала удаляет старые записи по регистратору, а затем добавляет новые. Это порождает большое количество «мертвых строк» (dead tuples), раздувает индексы и заставляет планировщик запросов выбирать неоптимальные планы.
Рассмотрим по шагам основные способы решения данной задачи.
Наиболее эффективный способ ускорить массовую запись движений — временно отключить актуализацию итогов регистра с помощью метода менеджера регистра УстановитьИспользованиеИтогов(). В этом режиме запись набора записей происходит практически мгновенно, так как платформа заполняет только таблицы движений и не тратит ресурсы на обновление остатков и оборотов.
Посмотрим на пример правильной организации кода с гарантированным возвратом использования итогов:
// Отключаем расчет итогов для ускорения записи
РегистрыБухгалтерии.Хозрасчетный.УстановитьИспользованиеИтогов(Ложь);
Попытка
НаборЗаписей = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Док);
НаборЗаписей.Прочитать();
// Вносим необходимые изменения в проводки или субконто
Для Каждого Запись Из НаборЗаписей Цикл
// Модификация данных записи
КонецЦикла;
// Отключаем обработчики событий и подписки для экономии ресурсов
НаборЗаписей.ОбменДанными.Загрузка = Истина;
НаборЗаписей.Записать();
// Включаем итоги обратно после успешной записи
РегистрыБухгалтерии.Хозрасчетный.УстановитьИспользованиеИтогов(Истина);
Исключение
// Гарантированно возвращаем итоги в случае ошибки
РегистрыБухгалтерии.Хозрасчетный.УстановитьИспользованиеИтогов(Истина);
ВызватьИсключение;
КонецПопытки;
При включении флага обратно платформа пересчитает только те итоги, которые были затронуты изменениями. Это занимает несоизмеримо меньше времени, чем пересчет «на лету» в момент построчной записи.
Обратите внимание на использование свойства ОбменДанными.Загрузка = Истина. Перед выполнением записи обязательно выставляйте данный флаг. Это полностью отключает исполнение процедур ПередЗаписью и ПриЗаписи модуля набора записей, а также блокирует все прикладные подписки на события. В типовых конфигурациях в подписках нередко выполняются сложные проверки заполнения реквизитов, обращение к дополнительным регистрам и фоновые расчеты, что сильно нагружает оперативную память сервера.
Если мы не хотим полностью выключать итоги по всей базе (например, параллельно работают пользователи, которым нужны актуальные отчеты), можно ограничить глубину пересчета. Для этого сдвинем границу рассчитанных итогов на начало месяца документа:
ГраницаИтогов = НачалоМесяца(Док.Дата);
РегистрыБухгалтерии.Хозрасчетный.УстановитьПериодРассчитанныхИтогов(ГраницаИтогов);
// Выполняем чтение и запись набора записей
НаборЗаписей.Записать();
// После завершения пересчитываем итоги точечно за нужный период
РегистрыБухгалтерии.Хозрасчетный.ПересчитатьИтогиЗаПериод(НачалоМесяца(Док.Дата), КонецМесяца(Док.Дата));
В этом случае платформа обновляет срез только за один расчетный месяц, избавляя СУБД от каскадного пересчета всех последующих периодов до текущего дня.
Если модифицируемый массив данных слишком велик и вызывает ошибки нехватки памяти (особенно в 32-разрядных процессах), разберем альтернативный алгоритм порционной перезаписи:
НаборЗаписей.Записать(Истина);НаборЗаписей.Записать(Ложь);Такой подход исключает длительные блокировки и позволяет СУБД фиксировать промежуточные транзакции меньшими порциями.
Если на сервере регулярно проводятся массивные регламентные процедуры, обратим внимание на следующие настройки СУБД:
VACUUM ANALYZE для таблиц движений и итогов регистра, чтобы удалить накопившиеся мертвые кортежи и обновить статистику для оптимизатора.