При обработке и записи крупных массивов данных (десятки и сотни тысяч строк) разработчики часто сталкиваются с ситуацией, когда первые несколько сотен записей формируются мгновенно, но затем скорость резко падает. При достижении 20 000 – 30 000 строк добавление каждой новой записи может занимать секунды, а весь процесс затягивается на долгие часы. Разберем по шагам, почему возникает эта проблема на уровне платформы 1С и как правильно оптимизировать код для быстрой работы.
Посмотрим на типичный пример кода, приводящий к лавинообразному замедлению:
Выборка = Результат.Выбрать();
Сч = 0;
Пока Выборка.Следующий() Цикл
Сч = Сч + 1;
Движение = Движения.МойРегистр.Добавить();
ЗаполнитьЗначенияСвойств(Движение, Выборка);
Если Сч % 100 = 0 Тогда
Состояние("Загружено " + Сч);
КонецЕсли;
КонецЦикла;
Движения.МойРегистр.Записать();
Замер производительности в платформе 1С показывает, что более 95–99% процессорного времени тратится вовсе не на обращение к базе данных, а на выполнение строки Движения.МойРегистр.Добавить() еще до момента вызова метода Записать().
Выясним причину такого поведения платформы:
РегистрНакопленияНаборЗаписей — это не легковесная таблица в оперативной памяти, а функциональная обертка платформы. При каждом вызове Добавить() платформа осуществляет проверку типов колонок, отслеживает флаги модифицированности, выполняет переиндексацию служебных коллекций и рассчитывает свойство НомерСтроки.Движения документа накладывает дополнительную нагрузку, так как набор связан с метаданными документа-регистратора и контекстом проведения.Самый эффективный и простой способ многократного ускорения — отказаться от построчного обхода выборки в пользу пакетной выгрузки результата запроса напрямую в набор записей с помощью метода Загрузить().
Разберем пример реализации:
// В запросе имена полей должны строго совпадать с именами измерений и ресурсов регистра
ТекстЗапроса =
"ВЫБРАТЬ
| Данные.Период КАК Период,
| Данные.Номенклатура КАК Номенклатура,
| Данные.Склад КАК Склад,
| Данные.Количество КАК Количество,
| Данные.Сумма КАК Сумма
|ИЗ
| ВременнаяТаблицаДанных КАК Данные";
Запрос = Новый Запрос(ТекстЗапроса);
ТаблицаДвижений = Запрос.Выполнить().Выгрузить();
// Пакетная загрузка в движения документа
Движения.МойРегистр.Загрузить(ТаблицаДвижений);
Движения.МойРегистр.Записать();
Почему этот подход работает в сотни раз быстрее:
Загрузить() в ядре платформы выделяет непрерывный блок памяти под весь массив строк единовременно, исключая постоянные перевыделения буфера.Если данные невозможно подготовить единым запросом и требуется сложная алгоритмическая обработка каждой строки в цикле, не следует добавлять строки напрямую в набор записей. Создадим промежуточную ТаблицаЗначений, сформируем строки в ней, а затем передадим итоговую таблицу в набор записей.
Посмотрим на пример реализации:
// Создаем независимый набор записей или используем движения документа
НаборЗаписей = РегистрыНакопления.МойРегистр.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаДокумент);
// Получаем пустую структуру таблицы движений с нужными типами колонок
ТаблицаБуфер = НаборЗаписей.ВыгрузитьКолонки();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
// Добавление строки в ТаблицуЗначений требует на порядки меньше ресурсов
НоваяСтрока = ТаблицаБуфер.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
// Выполняем требуемую прикладную логику обработки полей
НоваяСтрока.Период = ТекущаяДата();
НоваяСтрока.ВидДвижения = ВидДвиженияНакопления.Приход;
КонецЦикла;
// Загружаем готовую таблицу в набор записей
НаборЗаписей.Загрузить(ТаблицаБуфер);
НаборЗаписей.Записать();
Если объем данных превышает 50 000 – 100 000 строк, единовременная запись может перегрузить журнал транзакций СУБД и заблокировать таблицы итогов. В таких случаях запись необходимо выполнять порциями (по 5 000 – 10 000 строк) либо дробить один огромный документ на несколько связанных документов-регистраторов.
Проанализируем пример порционной записи через независимый набор записей с явным управлением транзакциями:
РазмерПорции = 5000;
ТаблицаДанных = РезультатЗапроса.Выгрузить();
ВсегоСтрок = ТаблицаДанных.Количество();
Индекс = 0;
Пока Индекс < ВсегоСтрок Цикл
// Выделяем порцию строк
ТаблицаПорции = ТаблицаДанных.Скопировать();
ТаблицаПорции.Очистить();
КонечныйИндекс = Мин(Индекс + РазмерПорции, ВсегоСтрок);
Для НомерСтроки = Индекс По КонечныйИндекс - 1 Цикл
ЗаполнитьЗначенияСвойств(ТаблицаПорции.Добавить(), ТаблицаДанных[НомерСтроки]);
КонецЦикла;
НачатьТранзакцию();
Попытка
Набор = РегистрыНакопления.МойРегистр.СоздатьНаборЗаписей();
Набор.Отбор.Регистратор.Установить(СсылкаНаРегистратор);
Набор.Загрузить(ТаблицаПорции);
// Параметр Ложь указывает, что существующие движения не очищаются перед записью
Набор.Записать(Ложь);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Индекс = Индекс + РазмерПорции;
КонецЦикла;
Когда узким местом становится непосредственное выполнение вызова Записать() в СУБД, основное время уходит на пересчет агрегатных таблиц остатков и оборотов. При начальной или регламентной пакетной загрузке сотен тысяч строк рекомендуется временно отключать итоги:
РегистрыНакопления.МойРегистр.УстановитьИспользованиеИтогов(Ложь);
РегистрыНакопления.МойРегистр.УстановитьИспользованиеИтогов(Истина);
РегистрыНакопления.МойРегистр.ПересчитатьИтоги();
Важное замечание: В свойствах самого регистра накопления в конфигураторе рекомендуется включить свойство «Разделение итогов». Это значительно сокращает взаимные блокировки СУБД при параллельной записи документов.
Использование метода Состояние() внутри серверного цикла (особенно при проведении документов) малоэффективно, так как без передачи управления на клиент пользовательский интерфейс все равно блокируется. Если операция выполняется на сервере, правильным архитектурным решением является запуск фонового задания с передачей прогресса выполнения через ХранилищеОбщихНастроек или параметры фонового задания, которые опрашиваются клиентом через обработчик ожидания.