Как сопоставить данные Таблицы Значений из Excel с базой данных 1С и загрузить их в документ без ошибок производительности?

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

При импорте данных из внешних источников (будь то файлы Excel, CSV или XML) разработчики 1С регулярно сталкиваются с классической задачей: загрузить данные во временную таблицу значений, найти соответствующие элементы в справочниках базы данных (например, по артикулу, штрихкоду или коду поставщика) и заполнить табличную часть создаваемого документа.

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

Разбор ошибки: почему исходный код автора не работал корректно?

Рассмотрим структуру исходного кода, который часто пишут начинающие разработчики:


Для Каждого стр из ТЗ Цикл
    Пока ВыборкаТовара.Следующий() Цикл
        ВыборкаСтрок = Поступление.Товары.Добавить();
        ВыборкаСтрок.Номенклатура = ВыборкаТовара.Товар;
        ВыборкаСтрок.Количество   = стр["Количество"];
        ВыборкаСтрок.Коэффициент  = 1;
    КонецЦикла;    
КонецЦикла;

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

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

Когда внешний цикл переходит ко второй строке таблицы значений ТЗ, внутренний цикл пытается снова вызвать метод ВыборкаТовара.Следующий(). Однако выборка уже полностью прочитана! Метод сразу возвращает значение Ложь, внутренний цикл не выполняется ни разу, и обработка последующих строк Excel полностью ломается. Данные сопоставляются некорректно.

Антипаттерн «Поиск в цикле»: почему это решение под запретом?

В процессе обсуждения на форумах часто предлагают «простое» решение проблемы — отказаться от предварительного запроса и искать элементы справочника прямо внутри цикла обхода строк Excel:


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

Хотя этот код выглядит лаконично и легко пишется, использовать поисковые методы базы данных (или запросы) внутри циклов категорически запрещено стандартами разработки 1С!

Проанализируем ситуацию на уровне СУБД. Если в загружаемом файле Excel содержится 1 000 строк, то при поиске в цикле платформа совершит ровно 1 000 отдельных транзакционных обращений к базе данных. Это вызовет огромные накладные расходы на сетевой обмен между сервером приложений и сервером СУБД, создаст избыточную нагрузку на процессоры и дисковую подсистему. Программа начнет сильно зависать даже на небольших файлах. База данных должна опрашиваться ровно один раз, после чего сопоставление должно происходить в оперативной памяти.

Способ 1. Сопоставление данных через объект «Соответствие» (Быстро и просто)

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

Разберем этот алгоритм по шагам:

  1. Выгружаем все уникальные артикулы/коды поставщика из загруженной таблицы значений ТЗ в отдельный массив или список значений.
  2. Передаем этот массив в качестве параметра в запрос. Запрос выбирает ссылки на товары и их уникальные коды за один проход.
  3. Обходим результат запроса и заполняем объект Соответствие, где в качестве Ключа указываем код поставщика, а в качестве Значения — ссылку на найденную номенклатуру.
  4. Запускаем цикл по строкам исходной таблицы значений ТЗ. Поиск в объекте Соответствие по ключу происходит в памяти практически мгновенно — за сложность O(1).

Посмотрим на практический пример реализации этого метода:


// Шаг 1: Извлекаем список артикулов из таблицы значений
МассивАртикулов = ТЗ.ВыгрузитьКолонку("Артикул");

// Убираем дубликаты для уменьшения объема передаваемых данных
ФункцияСвернутьМассив(МассивАртикулов); // Вспомогательный метод очистки дублей

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

Запрос.УстановитьПараметр("МассивАртикулов", МассивАртикулов);
РезультатЗапроса = Запрос.Выполнить();

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

// Шаг 4: Обходим ТЗ и быстро сопоставляем данные в памяти
Поступление = Документы.ПриходнаяНакладная.СоздатьДокумент();
Поступление.Дата = ДатаПоступления;
Поступление.Контрагент = Контрагент;

Для Каждого Стр Из ТЗ Цикл
    // Ищем товар в соответствии по ключу (Артикулу)
    НайденныйТовар = КартаТоваров.Получить(Стр.Артикул);
    
    Если НайденныйТовар <> Неопределено Тогда
        НоваяСтрока = Поступление.Товары.Добавить();
        НоваяСтрока.Номенклатура = НайденныйТовар;
        НоваяСтрока.Количество   = Стр.Количество;
        НоваяСтрока.Коэффициент  = 1;
    КонецЕсли;
КонецЦикла;

Поступление.Записать();

Использование объекта Соответствие гарантирует высокую скорость работы и полностью избавляет систему от проблемы "N+1 запросов". Этот код работает стабильно, надежно и легко читается.

Способ 2. Использование Временных Таблиц в запросе (Профессиональный подход)

Если вам необходимо загружать действительно большие объемы данных (десятки тысяч строк), наиболее производительным и масштабируемым решением будет передача всей таблицы значений непосредственно в запрос в качестве параметра с последующим соединением таблиц на уровне СУБД. В этом случае всю рутинную работу по сопоставлению берет на себя SQL-сервер, который оптимизирует процесс в разы лучше, чем встроенный язык 1С.

Рассмотрим важные нюансы, которые необходимо соблюдать при реализации данного метода:

  1. Строгая типизация колонок ТЗ: По умолчанию колонки в таблице значений, созданной при чтении Excel, могут иметь неопределенный или составной тип. Чтобы передать ТЗ в запрос, все задействованные колонки (в нашем примере это "Артикул" и "Количество") должны иметь жестко заданные типы данных.
  2. Индексация временной таблицы: Чтобы СУБД выполнила операцию соединения (JOIN) максимально быстро, временную таблицу внутри запроса необходимо обязательно проиндексировать по полю связи.

Разберем по шагам реализацию этого подхода:


// Шаг 1: Создаем типизированную Таблицу Значений для запроса
ТЗ_ДляЗапроса = Новый ТаблицаЗначений;
ТЗ_ДляЗапроса.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(50)));
ТЗ_ДляЗапроса.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));

// Заполняем типизированную таблицу данными из исходной ТЗ
Для Каждого СтрокаИсходная Из ТЗ Цикл
    ЗаполнитьЗначенияСвойств(ТЗ_ДляЗапроса.Добавить(), СтрокаИсходная);
КонецЦикла;

// Шаг 2: Создаем пакетный запрос с временной таблицей
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ДанныеExcel.Артикул КАК Артикул,
    |   ДанныеExcel.Количество КАК Количество
    |ПОМЕСТИТЬ ВТ_ДанныеExcel
    |ИЗ
    |   &ВнешнийИсточник КАК ДанныеExcel
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ИНДЕКСИРОВАТЬ ВТ_ДанныеExcel.Артикул
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |   КодыПоставщиков.Владелец КАК Номенклатура,
    |   ВТ_ДанныеExcel.Количество КАК Количество,
    |   1 КАК Коэффициент
    |ИЗ
    |   ВТ_ДанныеExcel КАК ВТ_ДанныеExcel
    |   ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.КодыПоставщиков КАК КодыПоставщиков
    |       ПО ВТ_ДанныеExcel.Артикул = КодыПоставщиков.КодТовара";

Запрос.УстановитьПараметр("ВнешнийИсточник", ТЗ_ДляЗапроса);

// Шаг 3: Выгружаем сопоставленные данные напрямую в табличную часть документа
РезультатТаблица = Запрос.Выполнить().Выгрузить();

Поступление = Документы.ПриходнаяНакладная.創りДокумент();
Поступление.Дата = ДатаПоступления;
Поступление.Контрагент = Контрагент;

// Метод Загрузить() мгновенно заполняет ТЧ документа результатами запроса без циклов!
Поступление.Товары.Загрузить(РезультатТаблица);
Поступление.Записать();

Этот способ является эталонным в экосистеме 1С. Мы полностью избавились от ручного перебора строк с помощью оператора Для Каждого на встроенном языке. Данные сопоставляются СУБД, а метод табличной части Загрузить() мгновенно заполняет документ. Этот подход минимизирует объем оперативной памяти сервера приложений 1С и обеспечивает максимальное быстродействие.

Сравнение методов сопоставления данных

Для наглядности сравним все три подхода, упомянутые в процессе обсуждения проблемы:

  1. Поиск в цикле (НайтиПоКоду/НайтиПоНаименованию): Чрезвычайно прост в написании, но ужасен по производительности. Подходит исключительно для отладки или работы с единичными записями. Полностью запрещен на рабочих базах.
  2. Использование объекта «Соответствие»: Оптимальный баланс между простотой написания и скоростью работы. Отлично показывает себя при объеме загрузки до 5 000 – 10 000 строк. Не требует сложной типизации ТЗ.
  3. Временная таблица в пакетном запросе: Самый профессиональный, производительный и расширяемый метод. Рекомендуется для промышленного использования, сложных интеграций и обработки больших массивов данных.

Выбирайте правильные методы разработки, пишите оптимальный код и создавайте быстрые, надежные решения в среде 1С!

← На главную