Как правильно и быстро выполнить массовую запись в регистр сведений 1С?

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

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

Проанализируем ситуацию, с которой столкнулся разработчик: необходимо перенести данные из табличной части документов «Заказ поставщику» в независимый периодический регистр сведений «Цены номенклатуры поставщиков». Подобные задачи часто встречаются, когда требуется сквозной перенос истории цен номенклатуры или автоматизированное заполнение табличной части «Товары» из поступлений для актуализации прайс-листов. Исходный код в примере ниже приводил к сбою системы.

Анализ исходного кода и его ключевые проблемы

Посмотрим на первоначальный вариант кода, который вызывал ошибки, и разберем, почему он неэффективен.


// Это пример НЕПРАВИЛЬНОГО кода
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЗаказПоставщикуТовары.Ссылка КАК Ссылка
    |ИЗ
    |    Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
    |ГДЕ
    |    ЗаказПоставщикуТовары.Ссылка.Дата > &Дата";
Запрос.УстановитьПараметр("Дата", '20201203235113'); 

РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    // Внутри первого цикла выполняется второй запрос
    запрос2 = новый запрос;
    запрос2.Текст =
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |    // ... поля документа ...
    |ИЗ
    |    Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
    |ГДЕ
    |    ЗаказПоставщикуТовары.Ссылка = &Ссылка";
    Запрос2.УстановитьПараметр("Ссылка", ВыборкаДетальныеЗаписи.Ссылка);
    РезультатЗапроса2 = Запрос2.Выполнить();    
    ВыборкаДетальныеЗаписи2 = РезультатЗапроса2.Выбрать();

    Пока ВыборкаДетальныеЗаписи2.Следующий() Цикл
        // Для КАЖДОЙ строки создается и записывается отдельный набор записей
        НаборЗаписей = РегистрыСведений.ЮТ_ЦеныНоменклатурыПоставщиковСПоставщиком.СоздатьНаборЗаписей(); 
        НаборЗаписей.Отбор.Регистр.Установить(ВыборкаДетальныеЗаписи2.Ссылка); 
        НоваяЗапись = НаборЗаписей.Добавить(); 
        // ... присвоение значений полям ...
        НоваяЗапись.Цена = ВыборкаДетальныеЗаписи2.Сумма/ВыборкаДетальныеЗаписи2.Количество;
        // ...
        НаборЗаписей.Записать();
    КонецЦикла;
КонецЦикла;

Этот код содержит несколько критических архитектурных ошибок, которые в совокупности приводят к коллапсу производительности. Рассмотрим их по порядку.

  1. Главная ошибка: Запрос в цикле. На каждой итерации внешнего цикла выполняется новый запрос к базе данных. Если у нас 1000 документов, будет выполнена 1001 транзакция с сервером (1 начальный запрос + 1000 запросов в цикле). Это создает колоссальную нагрузку на СУБД. В таких случаях полезно анализировать SQL сервер глазами 1С-ника, чтобы увидеть реальную очередь запросов. Правило №1 в оптимизации: проводите своевременную диагностику, используя замер производительности и поиск узких мест, и всегда избегайте запросов в циклах.
  2. Неэффективная работа с набором записей. Внутри второго цикла для каждой строки табличной части документа создается новый НаборЗаписей, в него добавляется одна запись, и он немедленно записывается. Это крайне медленно при больших объемах.
  3. Избыточный первый запрос. Первый запрос выбирает данные из табличной части. Если в документе 20 строк, внешний цикл выполнится 20 раз для одного и того же объекта. Чтобы исключить такие ситуации на этапе разработки, рекомендуется выполнять регулярный анализ конфигураций на наличие ошибок — для этого подойдёт набор инструментов для разработчика и анализатора метаданных.
  4. Лишние обращения к базе данных в цикле. Вызов НайтиПоНаименованию внутри цикла заставляет систему выполнять поиск на каждой итерации. Это значение следует вынести за пределы цикла.
  5. Потенциальные ошибки времени выполнения. В коде отсутствует проверка деления на ноль. Если количество окажется равно нулю, выполнение прервется.

Совокупность этих проблем и приводит к тому, что при обработке сотен документов система исчерпывает оперативную память и теряет соединение с сервером.

Решение №1: Оптимальный способ с использованием одного запроса

Самый правильный подход — получить все необходимые данные за одно обращение к базе данных, обработать их в памяти и записать одной транзакцией.

Шаг 1. Составляем единый запрос.


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЗаказПоставщикуТовары.Номенклатура,
    |    ЗаказПоставщикуТовары.Упаковка,
    |    ЗаказПоставщикуТовары.Ссылка.Валюта КАК Валюта,
    |    ЗаказПоставщикуТовары.ВидЦеныПоставщика,
    |    ЗаказПоставщикуТовары.Сумма,
    |    ЗаказПоставщикуТовары.Количество,
    |    ЗаказПоставщикуТовары.Ссылка.Дата КАК Период,
    |    ЗаказПоставщикуТовары.Ссылка.Партнер,
    |    ЗаказПоставщикуТовары.Ссылка КАК Регистратор
    |ИЗ
    |    Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
    |ГДЕ
    |    ЗаказПоставщикуТовары.Ссылка.Дата > &Дата
    |    И ЗаказПоставщикуТовары.Ссылка.Проведен"; 

Запрос.УстановитьПараметр("Дата", &НачальнаяДата); 

РезультатЗапроса = Запрос.Выполнить();
ТаблицаДанных = РезультатЗапроса.Выгрузить();

Шаг 2. Подготавливаем данные и записываем одним набором.


ВидЦеныПоУмолчанию = Справочники.ВидыЦенПоставщиков.НайтиПоНаименованию("Закупочная");
НаборЗаписей = РегистрыСведений.ЮТ_ЦеныНоменклатурыПоставщиковСПоставщиком.СоздатьНаборЗаписей();

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

    Если Строка.Количество <> 0 Тогда
        НоваяЗапись.Цена = Строка.Сумма / Строка.Количество;
    Иначе
        НоваяЗапись.Цена = 0;
    КонецЕсли;
КонецЦикла;

Попытка
    НаборЗаписей.Записать(Истина);
Исключение
    Сообщить("Не удалось записать данные в регистр: " + ОписаниеОшибки());
КонецПопытки;

Решение №2: Максимально быстрый способ через `НаборЗаписей.Загрузить()`

Для еще большей скорости можно использовать метод Загрузить(). Он позволяет загрузить данные в регистр напрямую из таблицы значений. Главное требование — названия колонок в таблице должны точно совпадать с названиями полей регистра.


// Шаг 1: Подготавливаем запрос с правильными псевдонимами полей
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЗаказПоставщикуТовары.Ссылка.Дата КАК Период,
    |    // ... остальные поля ...
    |    ВЫБОР
    |        КОГДА ЗаказПоставщикуТовары.Количество = 0 ТОГДА 0
    |        ИНАЧЕ ЗаказПоставщикуТовары.Сумма / ЗаказПоставщикуТовары.Количество
    |    КОНЕЦ КАК Цена
    |ИЗ ...";

// Шаг 2: Создаем набор и загружаем в него данные
ТаблицаДляЗагрузки = Запрос.Выполнить().Выгрузить();
НаборЗаписей = РегистрыСведений.ЮТ_ЦеныНоменклатурыПоставщиковСПоставщиком.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаДляЗагрузки);
НаборЗаписей.Записать(Истина);

Решение №3: Обработка очень больших объемов данных (порционная запись)

Если данных настолько много, что выгрузка в таблицу вызывает нехватку памяти, применяется пакетная обработка. Такие подходы незаменимы, когда вы, к примеру, ускоряете ЗУП 3 Корп на предприятиях с большим количеством работников, где расчетные механизмы оперируют миллионами строк. Идея проста: разбиваем задачу на маленькие периоды (дни или месяцы).


НачалоПериода = '20200101';
КонецПериода = '20231231';
ТекущаяДата = НачалоПериода;

Пока ТекущаяДата <= КонецПериода Цикл
    // Код обработки данных за один день (как в Решении №1)
    // ...
    ТекущаяДата = ДобавитьКДате(ТекущаяДата, "ДЕНЬ", 1);
    ОбработкаПрерыванияПользователя();
КонецЦикла;

Этот метод позволяет обрабатывать неограниченные объемы данных, так как в каждый момент времени в памяти находится только небольшая порция.

← На главную