Выполнение запросов внутри цикла при обходе строк табличной части или коллекций данных является одним из самых критичных антипаттернов в разработке на платформе 1С:Предприятие. Каждый вызов метода Выполнить() генерирует отдельное сетевое обращение к СУБД, что приводит к многократному увеличению нагрузки на сервер, возникновению избыточных блокировок и резкому падению производительности при росте количества строк.
Разберем типичный пример, проанализируем имеющиеся в нем ошибки и рассмотрим правильные варианты архитектурного решения задачи.
Посмотрим на исходный фрагмент кода, где для каждой строки табличной части вызывается отдельная функция с запросом:
&НаСервере
Процедура ЗаполнитьНаСервере(НовыйОбъект)
Товары = НовыйОбъект.Товары;
Для Каждого Строка из Товары Цикл
ПлановаяЦена = ПолучитьЗначениеЦеныПродажи(Строка.Номенклатура, НовыйОбъект.Ссылка.Дата, НовыйОбъект.Ссылка.Организация);
Строка.Себестоимость = Строка.Количество * ПлановаяЦена;
КонецЦикла;
КонецПроцедуры
Проанализируем основные недостатки такого подхода:
ОтчетПроизводстваЗаСменуПродукция.Ссылка.Организация) порождает на уровне SQL неявное левое соединение (LEFT JOIN), что создает лишнюю нагрузку на сервер.Проведен = ИСТИНА и ПометкаУдаления = ЛОЖЬ может вернуть неактуальные данные из непроведенных или удаленных документов.РегистрСведений.ЦеныНоменклатуры).Рассмотрим первый классический вариант решения: предварительно соберем список всей номенклатуры из табличной части в массив, передадим его в единый запрос с условием ГДЕ Номенклатура В (&МассивНоменклатуры), а результат выгрузим в структуру быстрого поиска.
Для быстрого поиска цены в оперативной памяти эффективнее всего использовать объект Соответствие, так как обращение по ключу выполняется со сложностью O(1), в отличие от поиска по таблице значений методом НайтиСтроки(), требующего создания индексов.
&НаСервере
Процедура ЗаполнитьСебестоимостьЧерезМассив(НовыйОбъект)
Товары = НовыйОбъект.Товары;
// Получаем массив ссылок на номенклатуру без дублей
МассивНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
// Если табличная часть пуста, дальнейшие действия не требуются
Если МассивНоменклатуры.Количество() = 0 Тогда
Возврат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Цены.Номенклатура КАК Номенклатура,
| Цены.ПлановаяСтоимость КАК Цена
|ИЗ
| Документ.ОтчетПроизводстваЗаСмену.Продукция КАК Цены
|ГДЕ
| Цены.Ссылка.Организация = &Организация
| И Цены.Ссылка.Проведен = ИСТИНА
| И Цены.Ссылка.ПометкаУдаления = ЛОЖЬ
| И Цены.Номенклатура В(&МассивНоменклатуры)
| И Цены.Ссылка.Дата МЕЖДУ &ДатаНач И &ДатаКон";
Запрос.УстановитьПараметр("Организация", НовыйОбъект.Организация);
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Запрос.УстановитьПараметр("ДатаНач", НачалоМесяца(НовыйОбъект.Дата));
Запрос.УстановитьПараметр("ДатаКон", КонецМесяца(НовыйОбъект.Дата));
Выборка = Запрос.Выполнить().Выбрать();
// Заполняем соответствие для быстрого доступа к ценам
СоответствиеЦен = Новый Соответствие;
Пока Выборка.Следующий() Цикл
СоответствиеЦен.Вставить(Выборка.Номенклатура, Выборка.Цена);
КонецЦикла;
// Заполняем табличную часть без выполнения запросов в цикле
Для Каждого СтрокаТЧ Из Товары Цикл
НайденнаяЦена = СоответствиеЦен[СтрокаТЧ.Номенклатура];
ПлановаяЦена = ?(НайденнаяЦена = Неопределено, 0, НайденнаяЦена);
СтрокаТЧ.Себестоимость = СтрокаТЧ.Количество * ПлановаяЦена;
КонецЦикла;
КонецПроцедуры
Разберем второй вариант, когда в запрос передается вся выгруженная табличная часть объекта. Этот подход позволяет связать строки документа с регистром сведений или таблицей документов с помощью соединения (ЛЕВОЕ СОЕДИНЕНИЕ) и сразу получить готовую результирующую таблицу для обратной загрузки.
Важный нюанс: при передаче таблицы значений в запрос рекомендуется индексировать временную таблицу по полям соединения с помощью директивы ИНДЕКСИРОВАТЬ ПО.
Посмотрим на пример с получением цен из периодического регистра сведений:
&НаСервере
Процедура ЗаполнитьЦеныИзРегистра(ДокументОбъект)
ТаблицаТовары = ДокументОбъект.Товары.Выгрузить();
Если ТаблицаТовары.Количество() = 0 Тогда
Возврат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.НомерСтроки КАК НомерСтроки,
| ТЧ.Номенклатура КАК Номенклатура,
| ТЧ.Количество КАК Количество,
| ЕСТЬNULL(СрезЦен.Цена, 0) КАК Цена,
| ТЧ.Количество * ЕСТЬNULL(СрезЦен.Цена, 0) КАК Себестоимость
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &ТаблицаТовары КАК ТЧ
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.НомерСтроки КАК НомерСтроки,
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Количество КАК Количество,
| ЕСТЬNULL(Цены.Цена, ВТ.Цена) КАК Цена,
| ВТ.Количество * ЕСТЬNULL(Цены.Цена, ВТ.Цена) КАК Себестоимость
|ИЗ
| ВТ_Товары КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &Период,
| Номенклатура В
| (ВЫБРАТЬ
| ВТ_Товары.Номенклатура
| ИЗ
| ВТ_Товары КАК ВТ_Товары)) КАК Цены
| ПО ВТ.Номенклатура = Цены.Номенклатура
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
Запрос.УстановитьПараметр("ТаблицаТовары", ТаблицаТовары);
Запрос.УстановитьПараметр("Период", ДокументОбъект.Дата);
Результат = Запрос.Выполнить();
// Загружаем готовый результат обратно в табличную часть документа
ДокументОбъект.Товары.Загрузить(Результат.Выгрузить());
КонецПроцедуры
ТаблицаЗначений через метод НайтиСтроки(), обязательно создавайте индекс по колонке поиска командой ТаблицаЗначений.Индексы.Добавить("Номенклатура"). Без индекса метод выполняет медленный полный перебор всех строк.СрезПоследних с наложением условий в параметрах виртуальной таблицы, а не ручным выборкам из документов.