При разработке обработок загрузки прайс-листов из внешних файлов (Excel, CSV) разработчики часто сталкиваются с резким падением производительности при увеличении объема строк. Обработка файла на 10 000–15 000 позиций может занимать десятки минут или даже приводить к зависанию сеанса. Проанализируем типичные узкие места в таком алгоритме и разберем по шагам, как ускорить импорт данных в разы.
Типичный сценарий загрузки включает в себя разбор строк файла, проверку наличия видов номенклатуры, создание карточек номенклатуры и формирование документа установки цен. Основные ошибки, замедляющие выполнение:
Справочники.ВидыЦен.НайтиПоНаименованию(...) на каждой итерации вызывают избыточные запросы к базе данных.Номенклатура.Ссылка или Группа.Ссылка заставляют платформу считывать весь объект из базы в память, если переменная уже содержит ссылку или объект.Номенклатура через НайтиПоНаименованию() в цикле на 10 000 строк превращается в 10 000 отдельных обращений к СУБД.ПолучитьCOMОбъект создает скрытые процессы, потребляет много ресурсов и работает медленнее платформенных механизмов.Рассмотрим первый шаг оптимизации. Все виды цен, единицы измерения, ставки НДС и служебные группы справочников необходимо получить один раз до входа в цикл обработки строк.
Посмотрим на пример кэширования видов цен в массив или структуру:
// Получаем ссылки заранее, до обработки данных
ВидыКоммерческихЦен = Новый Массив;
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("Прайс-листПоставщиков"));
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("КоммерческаяРозничная"));
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("КоммерческаяДилерская"));
ВидыКоммерческихЦен.Добавить(Справочники.ВидыЦен.НайтиПоНаименованию("КоммерческаяОптовая"));
ЕдиницаШт = Справочники.УпаковкиЕдиницыИзмерения.НайтиПоНаименованию("шт");
РодительТовары = Справочники.Номенклатура.НайтиПоНаименованию("Товары", Истина);
Разберем типичную ошибку при создании и поиске элементов. Если элемент найден, метод возвращает ссылку, а если создан — объект. Обращение Группа.Ссылка в случае, когда переменная уже является ссылкой, провоцирует избыточную нагрузку на СУБД.
Перепишем блок проверки и создания группы вида номенклатуры корректно:
ГруппаВида = Справочники.ВидыНоменклатуры.НайтиПоНаименованию(Объект.Поставщик);
Если НЕ ЗначениеЗаполнено(ГруппаВида) Тогда
ГруппаОбъект = Справочники.ВидыНоменклатуры.СоздатьГруппу();
ГруппаОбъект.Наименование = Объект.Поставщик;
ГруппаОбъект.Записать();
ГруппаВида = ГруппаОбъект.Ссылка;
КонецЕсли;
ВидНоменклатурыСсылка = Справочники.ВидыНоменклатуры.НайтиПоНаименованию(СокрЛП(Лев(НаименованиеИзФайла, 50)), Истина, ГруппаВида);
Если НЕ ЗначениеЗаполнено(ВидНоменклатурыСсылка) Тогда
ВидОбъект = Справочники.ВидыНоменклатуры.СоздатьЭлемент();
ВидОбъект.Родитель = ГруппаВида;
ВидОбъект.Наименование = СокрЛП(Лев(НаименованиеИзФайла, 50));
ВидОбъект.ТипНоменклатуры = Перечисления.ТипыНоменклатуры.Товар;
ВидОбъект.ВариантОформленияПродажи = Перечисления.ВариантыОформленияПродажи.РеализацияТоваровУслуг;
ВидОбъект.Записать();
ВидНоменклатурыСсылка = ВидОбъект.Ссылка;
КонецЕсли;
Для того чтобы не выполнять поиск по справочнику на каждой итерации, выгрузим существующую номенклатуру одним запросом перед началом обработки прайс-листа и проиндексируем поле поиска. Наличие индекса у ТаблицаЗначений переводит операцию Найти() из линейного перебора в мгновенный бинарный поиск.
Посмотрим, как инициализировать таблицу поиска:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
ТЗНоменклатура = Запрос.Выполнить().Выгрузить();
ТЗНоменклатура.Индексы.Добавить("Наименование");
При обнаружении новых позиций и их записи в базу необходимо сразу добавлять новую запись в кэширующую таблицу значений или использовать объект Соответствие, чтобы исключить дублирование элементов внутри одного файла:
СтрокаПоиска = ТЗНоменклатура.Найти(ТекущееНаименование, "Наименование");
Если СтрокаПоиска = Неопределено Тогда
НовыйОбъект = Справочники.Номенклатура.СоздатьЭлемент();
НовыйОбъект.Наименование = ТекущееНаименование;
НовыйОбъект.НаименованиеПолное = ТекущееНаименование;
НовыйОбъект.ВидНоменклатуры = ВидНоменклатурыСсылка;
НовыйОбъект.ЕдиницаИзмерения = ЕдиницаШт;
НовыйОбъект.Родитель = РодительТовары;
НовыйОбъект.Записать();
ТекущаяНоменклатура = НовыйОбъект.Ссылка;
// Актуализируем локальный кэш
НоваяСтрокаКэша = ТЗНоменклатура.Добавить();
НоваяСтрокаКэша.Номенклатура = ТекущаяНоменклатура;
НоваяСтрокаКэша.Наименование = ТекущееНаименование;
Иначе
ТекущаяНоменклатура = СтрокаПоиска.Номенклатура;
КонецЕсли;
Одной из наиболее тяжелых операций в исходном коде был поиск позиций, которые присутствовали в предыдущем документе установки цен, но отсутствуют в новом файле (для последующего обнуления цены). Исходный перебор двух коллекций через вложенный цикл занимал значительную часть времени.
Проанализируем оптимальный подход: получение номенклатуры из предыдущего документа цен выполняется прямым запросом к табличной части Документ.УстановкаЦенНоменклатуры.Товары без обращения через разыменование ссылки вложенного запроса.
&НаСервере
Функция ПолучитьОтсутствующиеПозиции(МассивНовыхПозиций, Поставщик, ВидЦены)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| УстановкаЦен.Ссылка КАК Ссылка
|ИЗ
| Документ.УстановкаЦенНоменклатуры КАК УстановкаЦен
|ГДЕ
| УстановкаЦен.Проведен = ИСТИНА
| И УстановкаЦен.Комментарий ПОДОБНО &Комментарий
|УПОРЯДОЧИТЬ ПО
| УстановкаЦен.Дата УБЫВ;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| ДокументТовары.Номенклатура КАК Номенклатура
|ИЗ
| Документ.УстановкаЦенНоменклатуры.Товары КАК ДокументТовары
|ГДЕ
| ДокументТовары.Ссылка В (&ПредыдущийДокумент)
| И ДокументТовары.ВидЦены = &ВидЦены";
Запрос.УстановитьПараметр("Комментарий", "Прайс-Лист " + Поставщик + "%");
Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
// Получаем ссылку на последний проведенный документ
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&ПредыдущийДокумент",
"ВЫБРАТЬ ПЕРВЫЕ 1 УстановкаЦен.Ссылка ИЗ Документ.УстановкаЦенНоменклатуры КАК УстановкаЦен " +
"ГДЕ УстановкаЦен.Проведен = ИСТИНА И УстановкаЦен.Комментарий ПОДОБНО &Комментарий УПОРЯДОЧИТЬ ПО УстановкаЦен.Дата УБЫВ");
// Выполняем выборку позиций предыдущего документа
ТаблицаСтарыхТоваров = Запрос.Выполнить().Выгрузить();
ТаблицаСтарыхТоваров.Индексы.Добавить("Номенклатура");
// Формируем соответствие новых позиций для мгновенного поиска
СоответствиеНовых = Новый Соответствие;
Для Каждого СсылкаНом Из МассивНовыхПозиций Цикл
СоответствиеНовых.Вставить(СсылкаНом, Истина);
КонецЦикла;
МассивОтсутствующих = Новый Массив;
Для Каждого Стр Из ТаблицаСтарыхТоваров Цикл
Если СоответствиеНовых[Стр.Номенклатура] = Неопределено Тогда
МассивОтсутствующих.Добавить(Стр.Номенклатура);
КонецЕсли;
КонецЦикла;
Возврат МассивОтсутствующих;
КонецФункции
Для замера производительности не требуется подключать внешние компоненты вроде MSScriptControl.ScriptControl, которые нарушают кроссплатформенность и не работают на 64-битных клиентах. В платформе 1С предусмотрен встроенный метод ТекущаяУниверсальнаяДатаВМиллисекундах():
ВремяНачала = ТекущаяУниверсальнаяДатаВМиллисекундах();
// Вызов тяжелой серверной процедуры
ЗагрузитьМетодом();
ВремяОкончания = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Загрузка выполнена за: " + (ВремяОкончания - ВремяНачала) + " мс.");
Применение кэширования ссылок, предварительная индексация таблицы поиска номенклатуры и исключение квадратичных переборов позволяют сократить время выполнения загрузки прайс-листов с десятков минут до нескольких секунд даже на больших объемах данных.