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