Одной из самых распространенных и критических ошибок при разработке на платформе 1С:Предприятие является выполнение запроса внутри цикла. Эта антипаттерн-практика способна замедлить работу системы в десятки и сотни раз, особенно когда количество итераций (строк в документе или элементов в списке) растет. В данной статье мы разберем, почему так происходит, и рассмотрим правильные методы оптимизации кода на конкретных примерах.
Давайте проанализируем ситуацию: каждый раз, когда вызывается метод Запрос.Выполнить(), система выполняет целый ряд тяжеловесных операций:
Если в табличной части вашего документа 100 строк, то вместо одного эффективного обращения к базе вы делаете 100 «мелких» вызовов, каждый из которых несет накладные расходы.
Рассмотрим код, который часто встречается у начинающих разработчиков:
Для Каждого СтрТЧ из Объект.Товары Цикл
ЗапросЦенаОстаток = Новый Запрос;
ЗапросЦенаОстаток.УстановитьПараметр("Номенклатура", СтрТЧ.Номенклатура);
ЗапросЦенаОстаток.УстановитьПараметр("ТекДата", ТекущаяДата());
ЗапросЦенаОстаток.Текст = "ВЫБРАТЬ ... ГДЕ Номенклатура = &Номенклатура";
Результат = ЗапросЦенаОстаток.Выполнить();
// ... обработка результата
КонецЦикла;
Здесь основная проблема в том, что запрос выполняется для каждой строки Объект.Товары. Наша задача — получить все данные за один вызов сервера.
Самый простой способ исправить ситуацию — это собрать все ссылки на номенклатуру в массив и передать его в запрос. Рассмотрим этот алгоритм по шагам:
Проанализируем пример реализации:
// 1. Собираем массив номенклатуры
МассивНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
// 2. Подготавливаем один запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыОстатки.Номенклатура КАК Номенклатура,
| ТоварыОстатки.Характеристика КАК Характеристика,
| ТоварыОстатки.Цена КАК Цена,
| ТоварыОстатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&ТекДата, Номенклатура В (&МассивНоменклатуры)) КАК ТоварыОстатки";
Запрос.УстановитьПараметр("ТекДата", ТекущаяДата());
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
// 3. Выполняем и выгружаем в таблицу для поиска
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
// Для ускорения поиска можно добавить индекс
ТаблицаРезультат.Индексы.Добавить("Номенклатура,Характеристика");
// 4. Теперь цикл по товарам не содержит запросов
Для Каждого СтрТЧ Из Объект.Товары Цикл
ПараметрыПоиска = Новый Структура("Номенклатура, Характеристика", СтрТЧ.Номенклатура, СтрТЧ.Характеристика);
НайденныеСтроки = ТаблицаРезультат.НайтиСтроки(ПараметрыПоиска);
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаДанных = НайденныеСтроки[0];
// Сравниваем цены и обновляем строку ТЧ
Если СтрТЧ.Цена <> СтрокаДанных.Цена Тогда
СтрТЧ.Цена = СтрокаДанных.Цена;
// Сообщаем пользователю (без модальных окон в цикле!)
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Для товара " + СтрТЧ.Номенклатура + " установлена актуальная цена.";
Сообщение.Сообщить();
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если в табличной части много строк или требуется соединение данных ТЧ с несколькими регистрами, лучше использовать Временные Таблицы (ВТ). Это позволяет передать всю структуру документа в СУБД и выполнить соединение на стороне сервера базы данных.
Рассмотрим, как это работает:
// 1. Выгружаем ТЧ в Таблицу Значений
ТЗ_Товары = Объект.Товары.Выгрузить();
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// 2. Помещаем данные ТЧ во временную таблицу
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Номенклатура КАК Номенклатура,
| ТЗ.Характеристика КАК Характеристика,
| ТЗ.НомерСтроки КАК НомерСтроки
|ПОМЕСТИТЬ ВТ_ТоварыДокумента
|ИЗ
| &ТЗ КАК ТЗ
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.НомерСтроки,
| Остатки.Цена,
| Остатки.КоличествоОстаток
|ИЗ
| ВТ_ТоварыДокумента КАК ВТ
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(&ТекДата,
| Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_ТоварыДокумента)) КАК Остатки
| ПО ВТ.Номенклатура = Остатки.Номенклатура
| И ВТ.Характеристика = Остатки.Характеристика";
Запрос.УстановитьПараметр("ТЗ", ТЗ_Товары);
Запрос.УстановитьПараметр("ТекДата", ТекущаяДата());
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Обращаемся напрямую к строке ТЧ по индексу (минус 1, так как НомерСтроки начинается с 1)
СтрТЧ = Объект.Товары[Выборка.НомерСтроки - 1];
СтрТЧ.Цена = Выборка.Цена;
КонецЦикла;
Использование временных таблиц считается наиболее правильным подходом, так как ВНУТРЕННЕЕ СОЕДИНЕНИЕ на уровне SQL работает гораздо быстрее, чем ручной поиск методом Найти() в коде 1С.
Проанализируем еще несколько моментов, которые помогут сделать ваш код безупречным:
Остатки(&Дата, Номенклатура В (&Массив)), а не в секцию ГДЕ. Это заставляет СУБД отсекать лишние данные еще на этапе чтения с диска, а не после загрузки всех остатков в память.Предупреждение() внутри цикла. Это крайне плохая практика: если цен изменится много, пользователь будет вынужден нажимать "ОК" сто раз. Используйте СообщениеПользователю, они не блокируют работу программы и аккуратно выводятся в специальном окне.ИНДЕКСИРОВАТЬ ПО в тексте запроса для полей, по которым идет соединение.Выяснив причину медленной работы и внедрив пакетную обработку данных, вы сможете обеспечить стабильную и быструю работу системы даже при очень больших объемах данных в документах.