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

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

При разработке обработок загрузки прайс-листов из внешних файлов (Excel, CSV) разработчики часто сталкиваются с резким падением производительности при увеличении объема строк. Обработка файла на 10 000–15 000 позиций может занимать десятки минут или даже приводить к зависанию сеанса. Проанализируем типичные узкие места в таком алгоритме и разберем по шагам, как ускорить импорт данных в разы.

Выясним причины низкой производительности

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

  1. Поиск предопределенных или постоянных ссылок внутри цикла: постоянные обращения вида Справочники.ВидыЦен.НайтиПоНаименованию(...) на каждой итерации вызывают избыточные запросы к базе данных.
  2. Неявные обращения к базе через точку (разыменование ссылок): конструкции вроде Номенклатура.Ссылка или Группа.Ссылка заставляют платформу считывать весь объект из базы в память, если переменная уже содержит ссылку или объект.
  3. Повторный поиск в базе данных: проверка существования элементов справочника Номенклатура через НайтиПоНаименованию() в цикле на 10 000 строк превращается в 10 000 отдельных обращений к СУБД.
  4. Вложенные циклы при поиске отсутствующих позиций: перебор строк старого документа внутри цикла по загружаемым позициям дает квадратичную сложность алгоритма O(N*M).
  5. Неоптимальная работа с COM-объектом Excel: использование ПолучитьCOMОбъект создает скрытые процессы, потребляет много ресурсов и работает медленнее платформенных механизмов.

Шаг 1. Кэширование постоянных ссылок перед циклом

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

Посмотрим на пример кэширования видов цен в массив или структуру:


// Получаем ссылки заранее, до обработки данных
ВидыКоммерческихЦен = Новый Массив;
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("Прайс-листПоставщиков"));
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("КоммерческаяРозничная"));
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("КоммерческаяДилерская"));
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("КоммерческаяОптовая"));

ЕдиницаШт = Справочники.УпаковкиЕдиницыИзмерения.НайтиПоНаименованию("шт");
РодительТовары = Справочники.Номенклатура.НайтиПоНаименованию("Товары", Истина);

Шаг 2. Исключение лишних обращений к базе и исправление работы с объектами

Разберем типичную ошибку при создании и поиске элементов. Если элемент найден, метод возвращает ссылку, а если создан — объект. Обращение Группа.Ссылка в случае, когда переменная уже является ссылкой, провоцирует избыточную нагрузку на СУБД.

Перепишем блок проверки и создания группы вида номенклатуры корректно:


ГруппаВида = Справочники.ВидыНоменклатуры.НайтиПоНаименованию(Объект.Поставщик);
Если НЕ ЗначениеЗаполнено(ГруппаВида) Тогда
    ГруппаОбъект = Справочники.ВидыНоменклатуры.СоздатьГруппу();
    ГруппаОбъект.Наименование = Объект.Поставщик;
    ГруппаОбъект.Записать();
    ГруппаВида = ГруппаОбъект.Ссылка;
КонецЕсли;

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

Шаг 3. Замена поиска в базе на индексированную таблицу значений или соответствие

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

Посмотрим, как инициализировать таблицу поиска:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Номенклатура.Ссылка КАК Номенклатура,
    |   Номенклатура.Наименование КАК Наименование
    |ИЗ
    |   Справочник.Номенклатура КАК Номенклатура";

ТЗНоменклатура = Запрос.Выполнить().Выгрузить();
ТЗНоменклатура.Индексы.Добавить("Наименование");

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


СтрокаПоиска = ТЗНоменклатура.Найти(ТекущееНаименование, "Наименование");

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

Шаг 4. Оптимизация поиска выбывших позиций

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

Проанализируем оптимальный подход: получение номенклатуры из предыдущего документа цен выполняется прямым запросом к табличной части Документ.УстановкаЦенНоменклатуры.Товары без обращения через разыменование ссылки вложенного запроса.


&НаСервере
Функция ПолучитьОтсутствующиеПозиции(МассивНовыхПозиций, Поставщик, ВидЦены)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |   УстановкаЦен.Ссылка КАК Ссылка
        |ИЗ
        |   Документ.УстановкаЦенНоменклатуры КАК УстановкаЦен
        |ГДЕ
        |   УстановкаЦен.Проведен = ИСТИНА
        |   И УстановкаЦен.Комментарий ПОДОБНО &Комментарий
        |УПОРЯДОЧИТЬ ПО
        |   УстановкаЦен.Дата УБЫВ;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ РАЗЛИЧНЫЕ
        |   ДокументТовары.Номенклатура КАК Номенклатура
        |ИЗ
        |   Документ.УстановкаЦенНоменклатуры.Товары КАК ДокументТовары
        |ГДЕ
        |   ДокументТовары.Ссылка В (&ПредыдущийДокумент)
        |   И ДокументТовары.ВидЦены = &ВидЦены";
        
    Запрос.УстановитьПараметр("Комментарий", "Прайс-Лист " + Поставщик + "%");
    Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
    
    // Получаем ссылку на последний проведенный документ
    Запрос.Текст = СтрЗаменить(Запрос.Текст, "&ПредыдущийДокумент", 
        "ВЫБРАТЬ ПЕРВЫЕ 1 УстановкаЦен.Ссылка ИЗ Документ.УстановкаЦенНоменклатуры КАК УстановкаЦен " +
        "ГДЕ УстановкаЦен.Проведен = ИСТИНА И УстановкаЦен.Комментарий ПОДОБНО &Комментарий УПОРЯДОЧИТЬ ПО УстановкаЦен.Дата УБЫВ");
    
    // Выполняем выборку позиций предыдущего документа
    ТаблицаСтарыхТоваров = Запрос.Выполнить().Выгрузить();
    ТаблицаСтарыхТоваров.Индексы.Добавить("Номенклатура");
    
    // Формируем соответствие новых позиций для мгновенного поиска
    СоответствиеНовых = Новый Соответствие;
    Для Каждого СсылкаНом Из МассивНовыхПозиций Цикл
        СоответствиеНовых.Вставить(СсылкаНом, Истина);
    КонецЦикла;
    
    МассивОтсутствующих = Новый Массив;
    Для Каждого Стр Из ТаблицаСтарыхТоваров Цикл
        Если СоответствиеНовых[Стр.Номенклатура] = Неопределено Тогда
            МассивОтсутствующих.Добавить(Стр.Номенклатура);
        КонецЕсли;
    КонецЦикла;
    
    Возврат МассивОтсутствующих;
КонецФункции

Шаг 5. Замер времени выполнения без сторонних COM-библиотек

Для замера производительности не требуется подключать внешние компоненты вроде MSScriptControl.ScriptControl, которые нарушают кроссплатформенность и не работают на 64-битных клиентах. В платформе 1С предусмотрен встроенный метод ТекущаяУниверсальнаяДатаВМиллисекундах():


ВремяНачала = ТекущаяУниверсальнаяДатаВМиллисекундах();

// Вызов тяжелой серверной процедуры
ЗагрузитьМетодом();

ВремяОкончания = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Загрузка выполнена за: " + (ВремяОкончания - ВремяНачала) + " мс.");

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

← На главную