Как оптимизировать алгоритм изменения регистра накопления при большом объеме данных?

Программист 1С v8.3 (Управляемые формы) 1С:ERP Управление предприятием Управленческий учет IT и автоматизация бизнеса
← На главную

В процессе сопровождения крупных баз данных 1С, таких как 1С:ERP или 1С:Комплексная автоматизация, специалисты часто сталкиваются с необходимостью массовой корректировки движений в регистрах накопления. Одной из самых трудоемких задач является исправление ошибок в регистре ОстаткиНоменклатурыВСебестоимости. Если выполнять обновление записей «в лоб», система может начать работать крайне медленно или вовсе зависнуть. Разберем подробно, почему так происходит и как ускорить этот процесс в десятки раз.

Анализ проблемы: почему стандартный цикл работает медленно?

Проанализируем фрагмент кода, который часто становится причиной «зависаний» системы:


Для каждого стр из Объект.Изменения Цикл
    Набор = РегистрыНакопления.ОстаткиНоменклатурыВСебестоимости.СоздатьНаборЗаписей();
    Набор.Отбор.Регистратор.Значение = стр.Регистратор;
    Набор.Прочитать();
    Для Каждого движ Из Набор Цикл
        Если стр.АналитикаУчетаНоменклатуры = движ.АналитикаУчетаНоменклатуры Тогда
            движ.Стоимость = стр.Контракт;
            Набор.Записать(); // Критическая ошибка!
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

Основная проблема здесь заключается в архитектуре работы СУБД и платформы 1С. Рассмотрим причины низкой производительности этого кода:

  1. Избыточное создание объектов: На каждой итерации цикла создается новый объект НаборЗаписей.
  2. Множественные обращения к базе: Метод Прочитать() выполняет SQL-запрос SELECT при каждом проходе.
  3. Запись внутри цикла: Это самая тяжелая операция. Вызов Набор.Записать() внутри цикла по строкам одного и того же регистратора приводит к тому, что система инициирует транзакцию, обновляет индексы и пересчитывает итоги столько раз, сколько строк содержится в таблице изменений.
  4. Неявные транзакции: Каждая отдельная запись — это отдельная транзакция на уровне SQL-сервера, что создает колоссальную нагрузку на дисковую подсистему.

Решение 1: Группировка данных по регистратору

Первое, что мы должны сделать — это минимизировать количество обращений к базе данных. Вместо того чтобы открывать набор записей для каждой строки изменений, мы сгруппируем наши изменения по документам (регистраторам). Рассмотрим, как это реализовать правильно.

Сначала подготовим таблицу изменений, свернув ее по регистратору, или обойдем ее таким образом, чтобы один регистратор обрабатывался ровно один раз. Разберем пример оптимизированного кода:


// Создадим копию таблицы и оставим только уникальные регистраторы
ТЗ_Регистраторы = Объект.Изменения.Выгрузить();
ТЗ_Регистраторы.Свернуть("Регистратор");

Для Каждого СтрРег Из ТЗ_Регистраторы Цикл
    
    // Создаем набор один раз для конкретного документа
    Набор = РегистрыНакопления.ОстаткиНоменклатурыВСебестоимости.СоздатьНаборЗаписей();
    Набор.Отбор.Регистратор.Установить(СтрРег.Регистратор);
    Набор.Прочитать();
    
    Модифицирован = Ложь;
    
    // Ищем все изменения, относящиеся к этому регистратору
    СтрокиИзменений = Объект.Изменения.НайтиСтроки(Новый Структура("Регистратор", СтрРег.Регистратор));
    
    Для Каждого СтрИзм Из СтрокиИзменений Цикл
        Для Каждого Движ Из Набор Цикл
            Если Движ.АналитикаУчетаНоменклатуры = СтрИзм.АналитикаУчетаНоменклатуры Тогда
                Движ.Стоимость = СтрИзм.Контракт;
                Модифицирован = Истина;
            КонецЕсли;
        КонецЦикла;
    КонецЦикла;
    
    // Записываем весь набор целиком ОДИН раз для документа
    Если Модифицирован Тогда
        Набор.Записать();
    КонецЕсли;
    
КонецЦикла;

Этот подход сокращает количество вызовов Набор.Записать() в десятки раз, так как запись происходит один раз на весь документ, а не на каждую строку.

Решение 2: Использование режима «Загрузка = Истина»

При записи движений в типовых конфигурациях (ERP, КА, УТ) часто срабатывают подписки на события, которые могут запускать сложные проверки или дополнительные расчеты. Чтобы избежать лишних затрат ресурсов, мы можем использовать свойство ОбменДанными.Загрузка.

Проанализируем, как это работает: установка Набор.ОбменДанными.Загрузка = Истина сообщает системе, что данные записываются «как есть», и выполнять дополнительные прикладные проверки не требуется. Это значительно ускоряет процесс.


Набор.ОбменДанными.Загрузка = Истина;
Набор.Записать();

Важный нюанс: Используйте этот метод осторожно. Если регистр имеет сложные связи (например, зависимые регистры через подписки), то при записи в режиме загрузки эти связи не обновятся автоматически.

Решение 3: Управление итогами регистра

Если нам необходимо исправить миллионы записей, даже оптимизированная запись по регистраторам может занять часы. Выясним причину: при каждой записи система обновляет таблицы итогов (агрегаты). Чтобы радикально ускорить процесс, можно временно отключить использование итогов.

Разберем последовательность действий:

  1. Отключаем использование итогов для регистра: РегистрыНакопления.ОстаткиНоменклатурыВСебестоимости.УстановитьИспользованиеИтогов(Ложь);
  2. Выполняем все наши записи в цикле (сгруппированные по регистратору).
  3. Включаем итоги обратно: РегистрыНакопления.ОстаткиНоменклатурыВСебестоимости.УстановитьИспользованиеИтогов(Истина);

При включении итогов платформа 1С один раз пересчитает все таблицы итогов, что гораздо быстрее, чем пересчитывать их инкрементально при каждой записи набора.

Решение 4: Использование явных транзакций

Для еще большего ускорения мы можем объединить обработку нескольких регистраторов в одну транзакцию. Это уменьшит количество «коммитов» (фиксаций) в базе данных SQL.


НачатьТранзакцию();
Счетчик = 0;

Для Каждого СтрРег Из ТЗ_Регистраторы Цикл
    // ... логика чтения и изменения набора ...
    Набор.Записать();
    
    Счетчик = Счетчик + 1;
    // Фиксируем транзакцию каждые 100 документов
    Если Счетчик % 100 = 0 Тогда
        ЗафиксироватьТранзакцию();
        НачатьТранзакцию();
    КонецЕсли;
КонецЦикла;

Если ТранзакцияАктивна() Тогда
    ЗафиксироватьТранзакцию();
КонецЕсли;

Оптимальный размер порции (в примере — 100) подбирается опытным путем в зависимости от мощности сервера и объема данных в одном документе.

Рекомендации для регистра себестоимости в ERP

Посмотрим на специфику регистра ОстаткиНоменклатурыВСебестоимости. Этот регистр тесно связан с документом Расчет себестоимости товаров. Проанализируем риски: если вы просто поменяете цифры в движениях, следующий же запуск регламентного документа «Расчет себестоимости» может перезаписать ваши правки своими расчетными данными.

Чтобы правки «выжили», рассмотрите следующие варианты:

Подводя итог: для максимального ускорения необходимо группировать данные по регистратору, выносить метод Записать() из внутренних циклов, использовать явные транзакции и режим Загрузка = Истина. Эти действия позволяют превратить многочасовое ожидание в работу, занимающую считанные минуты.

← На главную