Одной из самых распространенных причин низкой производительности конфигураций 1С является выполнение запросов внутри циклов. В этой статье мы подробно разберем, почему такая практика считается критической ошибкой, проанализируем механизмы взаимодействия платформы с базой данных и изучим проверенные способы оптимизации кода. Мы научимся переносить логику обработки данных из циклов в пакетные запросы и использовать временные таблицы для достижения максимального быстродействия.
Прежде чем мы перейдем к практическим примерам, давайте выясним причину возникновения проблемы. Каждый раз, когда вызывается метод Запрос.Выполнить(), платформа 1С инициирует обращение к СУБД (например, Microsoft SQL Server или PostgreSQL). Этот процесс включает в себя установку соединения, передачу текста запроса, его компиляцию на стороне сервера БД, поиск данных и передачу результата обратно в 1С. Если у нас 10 000 итераций в цикле, система выполнит 10 000 обращений. Это создает колоссальную нагрузку на сетевой трафик и ресурсы сервера.
Рассмотрим стандартную ситуацию: нам необходимо получить остатки товаров для списка позиций. Разберем типичный, но неэффективный пример кода, который мы будем оптимизировать:
Для Каждого СтрокаТовара Из ТаблицаТоваров Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.В НаличииОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура = &Номенклатура) КАК ТоварыНаСкладахОстатки";
Запрос.УстановитьПараметр("Номенклатура", СтрокаТовара.Номенклатура);
Результат = Запрос.Выполнить().Выгрузить();
// Обработка результата...
КонецЦикла;
Первый и самый простой способ оптимизации — собрать все необходимые значения (например, ссылки на номенклатуру) в один массив и передать его в качестве параметра запроса. Проанализируем, как это работает: вместо тысячи мелких запросов мы выполняем один большой запрос, который возвращает данные сразу по всем объектам.
Выполним следующие шаги для реализации этого метода:
Массив и заполним его уникальными значениями из нашего списка.ГДЕ или в параметрах виртуальной таблицы используем оператор В.ТаблицаЗначений.Посмотрим на пример кода:
МассивНоменклатуры = ТаблицаТоваров.ВыгрузитьКолонку("Номенклатура");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.ВНаличииОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (&МассивНоменклатуры)) КАК ТоварыНаСкладахОстатки";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
ТаблицаОстатков = Запрос.Выполнить().Выгрузить();
// Обязательно индексируем таблицу для ускорения поиска методом НайтиСтроки
ТаблицаОстатков.Индексы.Добавить("Номенклатура");
Для Каждого СтрокаТовара Из ТаблицаТоваров Цикл
ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаТовара.Номенклатура);
НайденныеСтроки = ТаблицаОстатков.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаТовара.Остаток = НайденныеСтроки[0].Остаток;
КонецЕсли;
КонецЦикла;
Если исходные данные уже находятся в объекте ТаблицаЗначений и их количество значительно, лучше всего передать всю таблицу в запрос целиком. Это позволит нам выполнить соединение данных прямо на стороне СУБД, что работает на порядок быстрее, чем поиск в коде 1С. Рассмотрим подробнее этот механизм.
Для реализации этого метода нам понадобится объект МенеджерВременныхТаблиц. Мы разберем по шагам, как загрузить данные в запрос:
ТаблицаЗначений, убедившись, что все типы колонок строго типизированы (это важно для корректной работы временных таблиц).МенеджерВременныхТаблиц и назначим его запросу.Проанализируем пример реализации:
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Передаем таблицу значений в качестве параметра
Запрос.Текст =
"ВЫБРАТЬ
| ВТ_Данные.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТЗ_Товаров КАК ВТ_Данные
|;
|
|ВЫБРАТЬ
| ВТ_ИсходныеДанные.Номенклатура,
| ЕСТЬNULL(ТоварыНаСкладахОстатки.ВНаличииОстаток, 0) КАК Остаток
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_ИсходныеДанные)) КАК ТоварыНаСкладахОстатки
| ПО ВТ_ИсходныеДанные.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура";
Запрос.УстановитьПараметр("ТЗ_Товаров", ТаблицаТоваров);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
Важный момент: использование конструкции Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_ИсходныеДанные) в параметрах виртуальной таблицы значительно эффективнее, чем простое соединение, так как СУБД сможет применить индексы регистра и отсечь лишние данные еще до выполнения основного соединения.
Когда мы работаем с очень большими объемами данных (сотни тысяч строк), загрузка всей таблицы значений в параметр запроса может привести к избыточному потреблению оперативной памяти. В таких случаях мы рекомендуем использовать порционную обработку данных. Выясним причину: передача слишком большой таблицы в &ТЗ_Товаров может вызвать ошибку переполнения памяти на сервере 1С.
Для решения этой задачи мы можем разбивать исходный массив на порции (например, по 1000 элементов) и выполнять запрос в цикле по этим порциям. Это "хороший" пример цикла, так как количество обращений к базе данных сокращается в 1000 раз по сравнению с построчной обработкой.
Посмотрим, как организовать порционную загрузку:
РазмерПорции = 1000;
ВсегоЭлементов = ТаблицаТоваров.Количество();
ТекущийИндекс = 0;
Пока ТекущийИндекс < ВсегоЭлементов Цикл
ПорцияМассива = Новый Массив;
Для Сч = 1 По РазмерПорции Цикл
Если ТекущийИндекс >= ВсегоЭлементов Тогда Прервать; КонецЕсли;
ПорцияМассива.Добавить(ТаблицаТоваров[ТекущийИндекс].Номенклатура);
ТекущийИндекс = ТекущийИндекс + 1;
КонецЦикла;
// Выполняем запрос для текущей порции
Запрос.УстановитьПараметр("МассивНоменклатуры", ПорцияМассива);
// ... обработка данных порции
КонецЦикла;
Помимо устранения запросов в циклах, обратите внимание на следующие функции и объекты, которые помогают в оптимизации:
Индексировать — при использовании временных таблиц (ПОМЕСТИТЬ) всегда индексируйте поля, по которым будет происходить соединение.ВыбратьРазрешенные — используйте это ключевое слово, если в системе настроены права доступа на уровне записей (RLS), чтобы избежать ошибок прав доступа при выполнении запроса.Сгруппировать По — всегда группируйте данные в запросе, чтобы не передавать дублирующиеся строки из СУБД в 1С.Проанализировав ситуацию, можно сделать вывод: правильное использование временных таблиц и механизмов пакетных запросов позволяет сократить время выполнения кода с нескольких минут до нескольких секунд. Мы рекомендуем всегда приступать к рефакторингу кода, если вы видите конструкцию Новый Запрос внутри оператора Для Каждого или Пока.