Как оптимизировать загрузку номенклатуры, характеристик, свойств и цен из Excel в 1С?

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

Приветствуем, коллеги! Давайте разберем одну из частых проблем в работе с 1С – медленную массовую загрузку данных из внешних источников, таких как Excel. Автор столкнулся с тем, что 25 000 элементов грузятся целый день, а 500 элементов – 10 минут. Мы выясним причины такой низкой производительности и предложим эффективные методы оптимизации, чтобы ваша загрузка выполнялась максимально быстро.

Выявляем узкие места в текущем коде

Прежде чем приступать к масштабной переработке, всегда необходимо провести замер производительности, чтобы точно определить "узкие места". Однако, даже без замера, анализируя предоставленный код, мы можем выделить наиболее очевидные причины низкой скорости:

  1. Многократные запросы к базе данных внутри цикла: Основная и наиболее критичная проблема. В каждой итерации цикла выполняется несколько вызовов НайтиПоНаименованию для Справочники.КлассификаторЕдиницИзмерения, Справочники.КатегорииНоменклатуры, Справочники.Номенклатура, ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения. Каждый такой вызов – это отдельный запрос к базе данных, который требует времени на установление соединения, выполнение поиска и передачу данных. Для 500 строк это уже минимум 2500 запросов, что крайне неэффективно.
  2. Создание справочных элементов в цикле, которые могут быть однотипными:

    ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения

    для "суффикса" и "производителя" создаются в каждой итерации цикла. Эти элементы должны быть уникальными по наименованию и типу, их создание для каждой строки данных из Excel – избыточно и замедляет процесс. То же касается Справочники.ЗначенияСвойствОбъектов – хотя сами значения могут быть уникальны для каждой номенклатуры, их владелец (ПВХ) должен быть уже существующим.
  3. Избыточное обновление НаборСвойствХарактеристики категории в цикле: В коде происходит получение объекта категории и обновление ее НаборСвойствХарактеристики в каждой итерации. Если набор свойств для категории не меняется от номенклатуры к номенклатуре, или если он изменяется только при создании новой категории, то такое частое обновление является неоптимальным.
  4. Общие операции записи объектов: Каждая операция Записать() (для категории, номенклатуры, характеристик, значений свойств) требует обращения к базе данных, запускает обработчики событий, подписки и проверки целостности. При массовой загрузке их количество становится критичным.

Рассмотрим примеры проблемного кода:


// Многократные запросы к БД в цикле
ЕдИзм = Справочники.КлассификаторЕдиницИзмерения.НайтиПоНаименованию(ЕдИзм);
НайденаКатегория = Справочники.КатегорииНоменклатуры.НайтиПоНаименованию(НаименованиеКатегории,Истина);
НайденаНоменклатура = Справочники.Номенклатура.НайтиПоНаименованию(НаименованиеНоменклатуры);
Производитель = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.НайтиПоНаименованию("Производитель", Истина);

// Избыточное создание ПВХ и значений в цикле
СуффиксОбъект = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.СоздатьЭлемент();
// ...
СуффиксОбъект.Записать();
ЗначениеРеквизита = Справочники.ЗначенияСвойствОбъектов.СоздатьЭлемент();
// ...
ЗначениеРеквизита.Записать();

// Избыточное обновление НаборСвойствХарактеристики категории в цикле
ОбКат = НайденаКатегория.ПолучитьОбъект();
обНаборСвойствХарактеристики = ОбКат.НаборСвойствХарактеристики.ПолучитьОбъект();
// ...
обНаборСвойствХарактеристики.Записать();

Ключевые принципы оптимизации массовой загрузки

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

  1. Минимизация обращений к базе данных в цикле: Это золотое правило для высокой производительности. Каждое обращение к серверу базы данных дорого стоит по времени. Мы стремимся собрать всю необходимую информацию из БД одним или несколькими запросами до начала основного цикла обработки данных из Excel, а затем работать с этими данными в оперативной памяти.
  2. Пакетная обработка данных: Вместо того чтобы создавать и записывать каждый объект по отдельности, мы стараемся собрать как можно больше операций записи в "пакеты" и выполнить их за один раз. Это может относиться к заполнению табличных частей документов или использованию временных таблиц в запросах для массового сопоставления данных.
  3. Использование специальных режимов записи: Платформа 1С предоставляет специальный режим загрузки данных, который отключает часть проверок и обработчиков событий, что значительно ускоряет запись.
  4. Предварительное кэширование данных: Если мы часто ищем одни и те же элементы справочников или значения, мы можем загрузить их в Соответствие или ТаблицуЗначений до начала цикла и производить поиск по ним в оперативной памяти, что на порядки быстрее.
  5. Параллелизация задач: Для очень больших объемов данных мы можем разделить процесс на несколько независимых частей и выполнять их одновременно с помощью фоновых заданий, используя многоядерные возможности сервера.

Пошаговая оптимизация кода: от анализа к решению

Давайте теперь применим эти принципы к коду автора, следуя общей стратегии: сначала считываем данные, затем ищем/кэшируем существующие элементы, создаем недостающие (и сразу их кэшируем), и только потом записываем все изменения.

  1. Шаг 1: Предварительная загрузка и кэширование справочников

    Мы заменим многочисленные вызовы НайтиПоНаименованию внутри цикла на предварительную загрузку всех необходимых справочных данных в Соответствие или ТаблицуЗначений до начала основного цикла. Это позволит выполнять поиск в оперативной памяти, что значительно быстрее.

    Пример для категорий, единиц измерения и предопределенных ПВХ:

    
    // Соответствие для кэширования существующих категорий номенклатуры:
    // Ключ: НаименованиеКатегории, Значение: СсылкаНаКатегорию
    СоответствиеКатегорий = Новый Соответствие();
    Запрос = Новый Запрос("ВЫБРАТЬ Ссылка, Наименование ИЗ Справочник.КатегорииНоменклатуры");
    ВыборкаКатегорий = Запрос.Выполнить().Выбрать();
    Пока ВыборкаКатегорий.Следующий() Цикл
        СоответствиеКатегорий.Вставить(СокрЛП(ВыборкаКатегорий.Наименование), ВыборкаКатегорий.Ссылка);
    КонецЦикла;
    
    // Соответствие для кэширования существующих единиц измерения:
    // Ключ: НаименованиеЕдиницы, Значение: СсылкаНаЕдиницуИзмерения
    СоответствиеЕдиницИзмерения = Новый Соответствие();
    Запрос = Новый Запрос("ВЫБРАТЬ Ссылка, Наименование ИЗ Справочник.КлассификаторЕдиницИзмерения");
    ВыборкаЕдиницИзмерения = Запрос.Выполнить().Выбрать();
    Пока ВыборкаЕдиницИзмерения.Следующий() Цикл
        СоответствиеЕдиницИзмерения.Вставить(СокрЛП(ВыборкаЕдиницИзмерения.Наименование), ВыборкаЕдиницИзмерения.Ссылка);
    КонецЦикла;
    
    // Ищем или создаем ПВХ "Суффикс" и "Производитель" до цикла.
    // Они должны быть созданы только один раз.
    СуффиксПВХ = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.НайтиПоНаименованию("Суффикс", Истина);
    Если СуффиксПВХ = Неопределено Тогда
        СуффиксОбъект = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.СоздатьЭлемент();
        СуффиксОбъект.Наименование = "Суффикс";
        СуффиксОбъект.Заголовок = "Суффикс";
        СуффиксОбъект.НаборСвойств = Справочники.НаборыДополнительныхРеквизитовИСведений.Справочник_ХарактеристикиНоменклатуры;
        СуффиксОбъект.ТипЗначения = Тип("СправочникСсылка.ЗначенияСвойствОбъектов");
        СуффиксОбъект.Виден = Истина;
        СуффиксОбъект.Доступен = Истина;
        СуффиксОбъект.Записать();
        СуффиксПВХ = СуффиксОбъект.Ссылка;
    КонецЕсли;
    
    ПроизводительПВХ = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.НайтиПоНаименованию("Производитель", Истина);
    Если ПроизводительПВХ = Неопределено Тогда
        ПроизводительОбъект = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.СоздатьЭлемент();
        ПроизводительОбъект.Наименование = "Производитель";
        ПроизводительОбъект.Заголовок = "Производитель";
        ПроизводительОбъект.НаборСвойств = Справочники.НаборыДополнительныхРеквизитовИСведений.Справочник_ХарактеристикиНоменклатуры;
        ПроизводительОбъект.ТипЗначения = Тип("СправочникСсылка.ЗначенияСвойствОбъектов");
        ПроизводительОбъект.Виден = Истина;
        ПроизводительОбъект.Доступен = Истина;
        ПроизводительОбъект.Записать();
        ПроизводительПВХ = ПроизводительОбъект.Ссылка;
    КонецЕсли;
    
    // Соответствие для кэширования существующих ЗначенийСвойствОбъектов
    // Ключ: (УИДВладельцаПВХ + "_" + НаименованиеЗначения), Значение: СсылкаНаЗначениеСвойствОбъектов
    СоответствиеЗначенийСвойств = Новый Соответствие();
    Запрос = Новый Запрос("ВЫБРАТЬ
                          |    ЗначенияСвойствОбъектов.Ссылка КАК Ссылка,
                          |    ЗначенияСвойствОбъектов.Владелец КАК Владелец,
                          |    ЗначенияСвойствОбъектов.Наименование КАК Наименование
                          |ИЗ
                          |    Справочник.ЗначенияСвойствОбъектов КАК ЗначенияСвойствОбъектов
                          |ГДЕ
                          |    ЗначенияСвойствОбъектов.Владелец В ИЕРАРХИИ(&СуффиксПВХ, &ПроизводительПВХ)");
    Запрос.УстановитьПараметр("СуффиксПВХ", СуффиксПВХ);
    Запрос.УстановитьПараметр("ПроизводительПВХ", ПроизводительПВХ);
    ВыборкаЗначенийСвойств = Запрос.Выполнить().Выбрать();
    Пока ВыборкаЗначенийСвойств.Следующий() Цикл
        Ключ = Строка(ВыборкаЗначенийСвойств.Владелец.УникальныйИдентификатор()) + "_" + СокрЛП(ВыборкаЗначенийСвойств.Наименование);
        СоответствиеЗначенийСвойств.Вставить(Ключ, ВыборкаЗначенийСвойств.Ссылка);
    КонецЦикла;
    

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

  2. Шаг 2: Массовый поиск существующей номенклатуры с помощью запросов и временных таблиц

    Для эффективного поиска существующей номенклатуры, а также характеристик, вместо НайтиПоНаименованию в цикле, мы будем использовать запросы с временными таблицами. Это позволяет сопоставить все наименования из Excel с данными в базе за один или несколько запросов.

    
    // Добавляем индекс строки для последующего сопоставления
    ТабЗначенийИзФайлаЭксель.Колонки.Добавить("ИндексСтроки", Новый ОписаниеТипов("Число"));
    Для Каждого СтрИзТЗ Из ТабЗначенийИзФайлаЭксель Цикл
        СтрИзТЗ.ИндексСтроки = ТабЗначенийИзФайлаЭксель.Индекс(СтрИзТЗ);
    КонецЦикла;
    
    МенеджерВТ = Новый МенеджерВременныхТаблиц;
    Запрос = Новый Запрос;
    Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
    
    // 1. Помещаем данные из Excel во временную таблицу
    Запрос.Текст =
        "ВЫБРАТЬ
        |    ТЗ.Наименование КАК НаименованиеНоменклатуры,
        |    ТЗ.Производитель КАК ПроизводительИзФайла,
        |    ТЗ.Суффикс КАК СуффиксИзФайла,
        |    ТЗ.ИндексСтроки
        |ПОМЕСТИТЬ ВТ_ДанныеИзExcel
        |ИЗ
        |    &ТабЗначенийИзФайлаЭксель КАК ТЗ";
    Запрос.УстановитьПараметр("ТабЗначенийИзФайлаЭксель", ТабЗначенийИзФайлаЭксель);
    Запрос.Выполнить(); // Выполняем создание временной таблицы
    
    // 2. Ищем существующую номенклатуру
    Запрос.Текст =
        "ВЫБРАТЬ
        |    ТЗ.ИндексСтроки,
        |    Номенклатура.Ссылка КАК СсылкаНоменклатуры,
        |    Номенклатура.Наименование КАК НаименованиеНоменклатуры
        |ПОМЕСТИТЬ ВТ_НайденнаяНоменклатура
        |ИЗ
        |    ВТ_ДанныеИзExcel КАК ТЗ
        |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Номенклатура
        |        ПО ТЗ.НаименованиеНоменклатуры = Номенклатура.Наименование";
    Запрос.Выполнить();
    
    // 3. Выгружаем все данные из Excel с уже найденными ссылками на номенклатуру (если есть)
    Запрос.Текст =
        "ВЫБРАТЬ
        |    ТЗ.ИндексСтроки,
        |    ТЗ.НаименованиеНоменклатуры,
        |    ТЗ.ПроизводительИзФайла,
        |    ТЗ.СуффиксИзФайла,
        |    ЕСТЬNULL(НайденнаяНоменклатура.СсылкаНоменклатуры, НЕОПРЕДЕЛЕНО) КАК СсылкаНоменклатуры
        |ИЗ
        |    ВТ_ДанныеИзExcel КАК ТЗ
        |        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_НайденнаяНоменклатура КАК НайденнаяНоменклатура
        |        ПО ТЗ.ИндексСтроки = НайденнаяНоменклатура.ИндексСтроки
        |УПОРЯДОЧИТЬ ПО ИндексСтроки"; // Сохраняем порядок
    РезультатСопоставленияНоменклатуры = Запрос.Выполнить().Выгрузить();
    
    // Добавляем колонку для кэширования созданной номенклатуры
    РезультатСопоставленияНоменклатуры.Колонки.Добавить("СозданнаяНоменклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
    РезультатСопоставленияНоменклатуры.Колонки.Добавить("СозданныеХарактеристики", Новый ОписаниеТипов("СправочникСсылка.ХарактеристикиНоменклатуры"));
    
    // Теперь основной цикл будет работать с РезультатСопоставленияНоменклатуры
    // и уже иметь СсылкуНоменклатуры, если она найдена.
    

    После выполнения этого запроса у нас будет таблица, где для каждой строки из Excel будет либо ссылка на существующую номенклатуру, либо НЕОПРЕДЕЛЕНО, если номенклатура не найдена. Далее мы будем проходить по этой таблице, создавая недостающую номенклатуру и характеристики. Созданные элементы мы будем кэшировать в самом РезультатСопоставленияНоменклатуры или в дополнительных Соответствие.

  3. Шаг 3: Использование режима загрузки ОбменДанными.Загрузка = Истина

    Этот флаг значительно ускоряет запись объектов, отключая часть проверок, событий и подписок на события, которые обычно выполняются при интерактивной записи. Это может дать существенный прирост производительности при массовой загрузке.

    
    Попытка
        ОбменДанными.Загрузка = Истина; // Включаем режим загрузки
    
        Для каждого стрИзExcelТЗ из РезультатСопоставленияНоменклатуры Цикл
            // ... Ваш оптимизированный код по созданию/поиску/записи ...
        КонецЦикла;
    
        // ... Запись документа установки цен ...
    
    Исключение
        // Обязательно обработайте возможные ошибки!
        Сообщить("Ошибка при загрузке: " + ОписаниеОшибки());
    КонецПопытки;
    ОбменДанными.Загрузка = Ложь; // ОЧЕНЬ ВАЖНО: сбросить флаг после завершения загрузки!
    

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

  4. Шаг 4: Оптимизация НаборСвойствХарактеристики категории

    Обновление НаборСвойствХарактеристики внутри цикла для каждой категории является избыточным. Если набор свойств для категории не меняется от номенклатуры к номенклатуре, его следует обновить один раз после цикла или при создании новой категории. Мы должны выполнять эту операцию только тогда, когда создается новая категория, или если мы уверены, что набор свойств для существующей категории должен быть изменен (но не в каждой итерации).

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

    Таким образом, мы гарантируем, что набор свойств категории обновляется только один раз для каждой категории, а не в каждой итерации цикла.

  5. Шаг 5: Параллельная загрузка (для больших объемов)

    Если объем данных очень большой (десятки и сотни тысяч), мы можем разделить файл Excel на несколько частей и обрабатывать их в фоновых заданиях. Это позволяет использовать ресурсы сервера более эффективно и сократить общее время загрузки за счет параллельного выполнения операций.

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

    Пример вызова фонового задания (процедура ЗагрузкаСПомощьюВнешнегоИсточникаДанныхНаСервере должна стать экспортной и может быть вызывана в отдельном модуле или в качестве общей процедуры):

    
    // Предположим, что ТабЗначенийИзФайлаЭксель уже содержит данные из файла
    РазмерПакета = 5000; // Определяем оптимальный размер пакета
    МассивФоновыхЗаданий = Новый Массив();
    
    Для Счетчик = 0 По ТабЗначенийИзФайлаЭксель.Количество() - 1 Шаг РазмерПакета Цикл
        // Создаем новую таблицу значений для текущего пакета
        ПакетДанных = Новый ТаблицаЗначений;
        Для Каждого Колонка Из ТабЗначенийИзФайлаЭксель.Колонки Цикл
            ПакетДанных.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения);
        КонецЦикла;
    
        // Копируем строки в пакет
        Для ИндексСтроки = Счетчик По Мин(Счетчик + РазмерПакета - 1, ТабЗначенийИзФайлаЭксель.Количество() - 1) Цикл
            НоваяСтрока = ПакетДанных.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяСтрока, ТабЗначенийИзФайлаЭксель[ИндексСтроки]);
        КонецЦикла;
    
        // Параметры для фонового задания
        ПараметрыФоновогоЗадания = Новый Структура;
        ПараметрыФоновогоЗадания.Вставить("ДанныеДляЗагрузки", ПакетДанных);
        // Можно также передать СуффиксПВХ, ПроизводительПВХ и другие кэшированные данные,
        // если они не очень большие или если их поиск в каждом задании допустим.
    
        // Вызываем фоновое задание
        ФоновоеЗадание = ФоновыеЗадания.Выполнить("ИмяВашейПроцедурыЗагрузкиПакетаНаСервере", ПараметрыФоновогоЗадания);
        МассивФоновыхЗаданий.Добавить(ФоновоеЗадание);
    КонецЦикла;
    
    // Ожидаем завершения всех фоновых заданий
    Для Каждого Задание Из МассивФоновыхЗаданий Цикл
        ЖдатьЗавершенияФоновогоЗадания(Задание);
    КонецЦикла;
    Сообщить("Загрузка завершена.");
    

    Внутри процедуры ИмяВашейПроцедурыЗагрузкиПакетаНаСервере будет находиться весь описанный выше оптимизированный код для обработки одного пакета данных. Обратите внимание, что для корректной работы фоновых заданий, процедура должна быть экспортной и определена в серверном модуле.

  6. Шаг 6: Документ "Установка цен номенклатуры"

    Автор уже корректно создает один документ Документы.УстановкаЦенНоменклатуры до начала цикла, заполняет его табличную часть Запасы в цикле и записывает, а затем проводит один раз в конце. Это правильный подход и здесь дополнительной оптимизации не требуется, так как работа с документом уже выполняется пакетно.

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

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

Таким образом, для эффективной оптимизации массовой загрузки данных, мы должны перестроить логику с построчной обработки на пакетную, минимизировать обращения к базе данных в цикле, использовать специальные режимы записи для массовых операций и по возможности применить параллельную обработку с помощью фоновых заданий.

Всегда начинайте с замера производительности, чтобы точно определить, какие изменения дают наибольший эффект. Это позволит вам сосредоточить усилия на самых критичных участках кода.

← На главную