При работе с большими объемами данных в 1С часто возникает задача массового переноса или создания записей в регистрах сведений. Неправильный подход к этой задаче может привести к серьезным проблемам с производительностью: система начинает «тормозить», потреблять огромное количество памяти и, в конечном итоге, аварийно завершать работу с ошибками «Недостаточно памяти» или «Потеряно соединение с сервером». Разберем на конкретном примере, как избежать этих проблем и написать эффективный код.
Проанализируем ситуацию, с которой столкнулся разработчик: необходимо перенести данные из табличной части документов «Заказ поставщику» в независимый периодический регистр сведений «Цены номенклатуры поставщиков». Подобные задачи часто встречаются, когда требуется сквозной перенос истории цен номенклатуры или автоматизированное заполнение табличной части «Товары» из поступлений для актуализации прайс-листов. Исходный код в примере ниже приводил к сбою системы.
Посмотрим на первоначальный вариант кода, который вызывал ошибки, и разберем, почему он неэффективен.
// Это пример НЕПРАВИЛЬНОГО кода
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказПоставщикуТовары.Ссылка КАК Ссылка
|ИЗ
| Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
|ГДЕ
| ЗаказПоставщикуТовары.Ссылка.Дата > &Дата";
Запрос.УстановитьПараметр("Дата", '20201203235113');
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Внутри первого цикла выполняется второй запрос
запрос2 = новый запрос;
запрос2.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| // ... поля документа ...
|ИЗ
| Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
|ГДЕ
| ЗаказПоставщикуТовары.Ссылка = &Ссылка";
Запрос2.УстановитьПараметр("Ссылка", ВыборкаДетальныеЗаписи.Ссылка);
РезультатЗапроса2 = Запрос2.Выполнить();
ВыборкаДетальныеЗаписи2 = РезультатЗапроса2.Выбрать();
Пока ВыборкаДетальныеЗаписи2.Следующий() Цикл
// Для КАЖДОЙ строки создается и записывается отдельный набор записей
НаборЗаписей = РегистрыСведений.ЮТ_ЦеныНоменклатурыПоставщиковСПоставщиком.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистр.Установить(ВыборкаДетальныеЗаписи2.Ссылка);
НоваяЗапись = НаборЗаписей.Добавить();
// ... присвоение значений полям ...
НоваяЗапись.Цена = ВыборкаДетальныеЗаписи2.Сумма/ВыборкаДетальныеЗаписи2.Количество;
// ...
НаборЗаписей.Записать();
КонецЦикла;
КонецЦикла;
Этот код содержит несколько критических архитектурных ошибок, которые в совокупности приводят к коллапсу производительности. Рассмотрим их по порядку.
НаборЗаписей, в него добавляется одна запись, и он немедленно записывается. Это крайне медленно при больших объемах.НайтиПоНаименованию внутри цикла заставляет систему выполнять поиск на каждой итерации. Это значение следует вынести за пределы цикла.Совокупность этих проблем и приводит к тому, что при обработке сотен документов система исчерпывает оперативную память и теряет соединение с сервером.
Самый правильный подход — получить все необходимые данные за одно обращение к базе данных, обработать их в памяти и записать одной транзакцией.
Шаг 1. Составляем единый запрос.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказПоставщикуТовары.Номенклатура,
| ЗаказПоставщикуТовары.Упаковка,
| ЗаказПоставщикуТовары.Ссылка.Валюта КАК Валюта,
| ЗаказПоставщикуТовары.ВидЦеныПоставщика,
| ЗаказПоставщикуТовары.Сумма,
| ЗаказПоставщикуТовары.Количество,
| ЗаказПоставщикуТовары.Ссылка.Дата КАК Период,
| ЗаказПоставщикуТовары.Ссылка.Партнер,
| ЗаказПоставщикуТовары.Ссылка КАК Регистратор
|ИЗ
| Документ.ЗаказПоставщику.Товары КАК ЗаказПоставщикуТовары
|ГДЕ
| ЗаказПоставщикуТовары.Ссылка.Дата > &Дата
| И ЗаказПоставщикуТовары.Ссылка.Проведен";
Запрос.УстановитьПараметр("Дата", &НачальнаяДата);
РезультатЗапроса = Запрос.Выполнить();
ТаблицаДанных = РезультатЗапроса.Выгрузить();
Шаг 2. Подготавливаем данные и записываем одним набором.
ВидЦеныПоУмолчанию = Справочники.ВидыЦенПоставщиков.НайтиПоНаименованию("Закупочная");
НаборЗаписей = РегистрыСведений.ЮТ_ЦеныНоменклатурыПоставщиковСПоставщиком.СоздатьНаборЗаписей();
Для Каждого Строка Из ТаблицаДанных Цикл
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Период = Строка.Период;
НоваяЗапись.Номенклатура = Строка.Номенклатура;
НоваяЗапись.Партнер = Строка.Партнер;
НоваяЗапись.Упаковка = Строка.Упаковка;
НоваяЗапись.Валюта = Строка.Валюта;
НоваяЗапись.Регистратор = Строка.Регистратор;
Если ЗначениеЗаполнено(Строка.ВидЦеныПоставщика) Тогда
НоваяЗапись.ВидЦены = Строка.ВидЦеныПоставщика;
Иначе
НоваяЗапись.ВидЦены = ВидЦеныПоУмолчанию;
КонецЕсли;
Если Строка.Количество <> 0 Тогда
НоваяЗапись.Цена = Строка.Сумма / Строка.Количество;
Иначе
НоваяЗапись.Цена = 0;
КонецЕсли;
КонецЦикла;
Попытка
НаборЗаписей.Записать(Истина);
Исключение
Сообщить("Не удалось записать данные в регистр: " + ОписаниеОшибки());
КонецПопытки;
Для еще большей скорости можно использовать метод Загрузить(). Он позволяет загрузить данные в регистр напрямую из таблицы значений. Главное требование — названия колонок в таблице должны точно совпадать с названиями полей регистра.
// Шаг 1: Подготавливаем запрос с правильными псевдонимами полей
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказПоставщикуТовары.Ссылка.Дата КАК Период,
| // ... остальные поля ...
| ВЫБОР
| КОГДА ЗаказПоставщикуТовары.Количество = 0 ТОГДА 0
| ИНАЧЕ ЗаказПоставщикуТовары.Сумма / ЗаказПоставщикуТовары.Количество
| КОНЕЦ КАК Цена
|ИЗ ...";
// Шаг 2: Создаем набор и загружаем в него данные
ТаблицаДляЗагрузки = Запрос.Выполнить().Выгрузить();
НаборЗаписей = РегистрыСведений.ЮТ_ЦеныНоменклатурыПоставщиковСПоставщиком.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаДляЗагрузки);
НаборЗаписей.Записать(Истина);
Если данных настолько много, что выгрузка в таблицу вызывает нехватку памяти, применяется пакетная обработка. Такие подходы незаменимы, когда вы, к примеру, ускоряете ЗУП 3 Корп на предприятиях с большим количеством работников, где расчетные механизмы оперируют миллионами строк. Идея проста: разбиваем задачу на маленькие периоды (дни или месяцы).
НачалоПериода = '20200101';
КонецПериода = '20231231';
ТекущаяДата = НачалоПериода;
Пока ТекущаяДата <= КонецПериода Цикл
// Код обработки данных за один день (как в Решении №1)
// ...
ТекущаяДата = ДобавитьКДате(ТекущаяДата, "ДЕНЬ", 1);
ОбработкаПрерыванияПользователя();
КонецЦикла;
Этот метод позволяет обрабатывать неограниченные объемы данных, так как в каждый момент времени в памяти находится только небольшая порция.