Работа со сверхбольшими объемами данных в платформе 1С:Предприятие всегда требует от разработчика понимания внутренних механизмов платформы и используемой СУБД. Попытка записать более миллиона записей в независимый регистр сведений «в один запуск» — классический сценарий, приводящий к аварийному завершению работы рабочего процесса сервера 1С (падению по недостатку памяти) или зависанию СУБД. В этой статье мы подробно разберем, почему возникает эта проблема, как правильно реализовать порционную запись, оптимизировать настройки СУБД (особенно PostgreSQL) и использовать новые возможности платформы 1С версий 8.3.25 и 8.3.26.
Прежде чем перейти к написанию кода, давайте проанализируем физический смысл происходящего на сервере при попытке записать 1.5 млн строк за один раз:
РегистрСведенийНаборЗаписей, платформа сериализует эти данные в ОЗУ. Память расходуется как под саму таблицу, так и под метаданные набора записей. При превышении лимитов оперативной памяти рабочий процесс rphost просто перезапускается операционной системой.Для решения этой проблемы мы рассмотрим комплексный подход, состоящий из правильного разделения данных на порции, оптимизации СУБД и отключения сопутствующих механизмов платформы.
Самый надежный способ избежать падения сервера 1С — это разбиение всего объема данных на порции (батчи) оптимального размера. Оптимальным размером одной порции для большинства систем является диапазон от 10 000 до 50 000 записей. Меньший размер приведет к лишним накладным расходам на открытие/закрытие транзакций, больший — к риску нехватки памяти.
Рассмотрим правильный алгоритм порционной записи. Основная ошибка начинающих разработчиков заключается в использовании метода Загрузить() без понимания его специфики, либо в построчной записи в цикле (что работает крайне медленно из-за постоянных обращений к СУБД). Метод Загрузить() у объекта типа РегистрСведенийНаборЗаписей просто заполняет коллекцию в памяти, но физическая запись происходит только при вызове метода Записать().
Давайте разберем по шагам правильный шаблон кода для порционной записи из исходной таблицы значений ТаблицаВывод:
// Определяем размер порции
РазмерПорции = 20000;
ВсегоСтрок = ТаблицаВывод.Количество();
ТекущаяСтрока = 0;
Пока ТекущаяСтрока < ВсегоСтрок Цикл
// Создаем набор записей для каждой порции
НаборЗаписей = РегистрыСведений.ИмяВашегоРегистра.СоздатьНаборЗаписей();
// Ограничиваем транзакцию рамками одной порции
НачатьТранзакцию();
Попытка
// Заполняем порцию данных
Для Счетчик = 1 По РазмерПорции Цикл
Если ТекущаяСтрока >= ВсегоСтрок Тогда
Прервать;
КонецЕсли;
СтрокаИсточника = ТаблицаВывод[ТекущаяСтрока];
НоваяСтрока = НаборЗаписей.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточника);
ТекущаяСтрока = ТекущаяСтрока + 1;
КонецДля;
// Записываем порцию. Параметр Ложь указывает, что мы НЕ замещаем весь регистр,
// а дозаписываем данные в соответствии с установленными отборами (если они есть).
НаборЗаписей.Записать(Ложь);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("МассоваяЗагрузка",
УровеньЖурналаРегистрации.Ошибка,,,
ОписаниеОшибки());
ВызватьИсключение "Ошибка при записи порции данных: " + ОписаниеОшибки();
КонецПопытки;
// Даем серверу "передохнуть" и освободить память (при необходимости)
#Если Сервер Тогда
// Небольшая пауза или вызов обработчика ожидания для очистки контекста rphost
#КонецЕсли
КонецЦикла;
В этом коде критически важно использовать явное управление транзакциями через НачатьТранзакцию() и ЗафиксироватьТранзакцию(). Если этого не сделать, СУБД будет открывать и закрывать неявную транзакцию на каждую строку или пакет, что увеличит время выполнения в десятки раз из-за постоянных дисковых операций сброса кэша.
Если вы работаете на современных версиях платформы 1С:Предприятие 8.3.25 и выше, вам больше не нужно вручную вычитывать данные для сверки уникальности ключей в оперативной памяти сервера приложений. В платформе появились революционные режимы записи независимых регистров сведений через перечисление РежимЗамещения:
РежимЗамещения.Слияние (Merge) — доступен с версии 8.3.25. При записи набора СУБД на своем уровне выполняет операцию сопоставления. Существующие записи (по ключевым полям-измерениям) обновляются, а новые — добавляются. Это исключает необходимость предварительного удаления или ручной проверки «есть ли уже такая запись в базе».РежимЗамещения.Обновление (Update) — доступен с версии 8.3.26. В этом режиме СУБД обновляет только те записи, которые уже физически существуют в регистре. Новые записи полностью игнорируются СУБД, что работает в разы быстрее, так как не вызывает перестроения индексов таблиц на добавление.Применение этих режимов на уровне платформы транслируется в эффективные SQL-запросы типа MERGE (в MS SQL) или INSERT ... ON CONFLICT (в PostgreSQL), существенно снижая нагрузку на оперативную память.
В практическом опыте многие разработчики сталкиваются с ситуацией, когда одна и та же загрузка отлично отрабатывает на MS SQL Server, но намертво вешает PostgreSQL уже на первой сотне тысяч записей. Это связано с архитектурными различиями СУБД.
PostgreSQL использует механизм многоверсионности (MVCC). При модификации (UPDATE) или удалении (DELETE) старая версия строки физически остается в таблице в виде «мертвой строки» (Dead Tuple). При порционной записи больших объемов данных генерация этих мертвых строк происходит лавинообразно. Если СУБД настроена некорректно, стандартный механизм очистки Autovacuum может запуститься прямо во время вашей транзакции, заблокировать таблицы и вызвать жесткое зависание системы.
Для успешной загрузки миллионов строк на PostgreSQL выполните следующие настройки в файле postgresql.conf:
max_wal_size в значение не менее 2GB – 4GB. Это позволит СУБД реже выполнять принудительные контрольные точки (checkpoints), «замораживающие» дисковый ввод-вывод.autovacuum_vacuum_scale_factor = 0.05 (или даже 0.01 для сверхнагруженных таблиц), чтобы очистка мусора происходила чаще, но мелкими порциями, не накапливая миллионы мертвых строк.synchronous_commit = off. Это ускорит пакетную вставку данных в PostgreSQL примерно на 30-40%.maintenance_work_mem (например, до 1–2 Гб), чтобы СУБД могла эффективно перестраивать индексы по окончании записи порций.Существуют два платформенных механизма, которые способны замедлить запись данных в десятки раз и привести к дедлокам на уровне базы данных:
<ИмяРегистра>.Изменения).
НаборЗаписей.ОбменДанными.Получатели.Автозаполнение = Ложь;
После загрузки всех данных можно зарегистрировать изменения на узлах обмена одним общим пакетом, если это необходимо.Сведем все рекомендации в единый, готовый к использованию шаблон кода, учитывающий отключение авторегистрации изменений и эффективную работу с транзакциями:
Процедура ЗагрузитьДанныеВРегистрПорциями(ТаблицаИсточника) Экспорт
РазмерПорции = 15000;
ВсегоСтрок = ТаблицаИсточника.Количество();
ТекущаяСтрока = 0;
// Отключаем транзакции во время подготовки, управляем ими вручную
Пока ТекущаяСтрока < ВсегоСтрок Цикл
НаборЗаписей = РегистрыСведений.ИмяВашегоРегистра.СоздатьНаборЗаписей();
НачатьТранзакцию(РежимБлокировкиДанных.Управляемый);
Попытка
// Наполнение порции
Для Счетчик = 1 По РазмерПорции Цикл
Если ТекущаяСтрока >= ВсегоСтрок Тогда
Прервать;
КонецЕсли;
СтрокаТаблицы = ТаблицаИсточника[ТекущаяСтрока];
НоваяЗапись = НаборЗаписей.Добавить();
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТаблицы);
ТекущаяСтрока = ТекущаяСтрока + 1;
КонецДля;
// Важно: Отключаем регистрацию изменений для планов обмена на время загрузки
НаборЗаписей.ОбменДанными.Получатели.Автозаполнение = Ложь;
НаборЗаписей.ОбменДанными.Загрузка = Истина;
// Записываем набор без замещения существующих данных вне нашего набора
// Если платформа 8.3.25+, можно использовать:
// НаборЗаписей.Записать(РежимЗамещенияРегистраСведений.Слияние);
НаборЗаписей.Записать(Ложь);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("МассоваяЗагрузка",
УровеньЖурналаРегистрации.Предупреждение,,,
"Ошибка на строке " + ТекущаяСтрока + ": " + ОписаниеОшибки());
ВызватьИсключение; // Передаем ошибку выше для остановки процесса
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Применение предложенных подходов гарантирует стабильную, быструю и предсказуемую запись миллионов строк в независимые регистры сведений на любой СУБД без риска уронить рабочий процесс сервера 1С.