В среде разработчиков 1С утверждение «запрос в цикле — это зло» давно стало аксиомой. Однако начинающие специалисты часто задаются вопросом: почему код, который работает «моментально» на тестовой базе, вызывает гнев коллег и экспертов по производительности? В этой статье мы подробно разберем техническую сторону вопроса, проанализируем влияние таких запросов на серверную часть и выясним, когда из этого правила можно (и нужно) делать исключения.
Рассмотрим ситуацию, описанную автором темы: получение цен номенклатуры по нескольким складам. Если мы выполняем запрос в цикле по каждому складу, мы создаем так называемую проблему N+1. Давайте разберем, что происходит «под капотом» системы при каждом обращении к базе данных:
РезультатЗапроса.Если один запрос выполняется за 0.01 секунды, это кажется ничтожным. Но если складов или товаров становится 100 000 (как в примере из сообщения 85), суммарное время составит более 15 минут. Основная потеря времени здесь — не работа самой базы, а суммарные накладные расходы на организацию каждого вызова.
Для понимания эффективности часто используют наглядную аналогию с лифтом. Представьте, что вам нужно поднять 20 коробок на 10-й этаж. Вы можете положить все 20 коробок в лифт и подняться один раз (один запрос к БД). Или вы можете брать по одной коробке, заходить в лифт, подниматься, возвращаться и так 20 раз. Очевидно, что во втором случае вы потратите основное время не на перенос груза, а на ожидание лифта и его движение между этажами. Запрос в цикле — это и есть «хождение с одной коробкой».
Особое внимание стоит уделить механизму RLS (Record Level Security). Когда в базе настроены права доступа на уровне записей, платформа 1С автоматически модифицирует каждый ваш запрос, добавляя в него сложные соединения с таблицами прав. Если вы выполняете запрос в цикле 1000 раз, СУБД будет вынуждена 1000 раз проверять права доступа, что создает колоссальную нагрузку. В случае одного запроса проверка прав отработает один раз для всего результирующего набора данных.
Кроме того, при частом выполнении мелких запросов с разными параметрами происходит «замусоривание» кэша планов запросов СУБД. SQL-серверу сложнее оптимизировать выполнение, так как он видит поток разрозненных команд вместо одной логически завершенной операции.
Несмотря на общие запреты, существуют ситуации, когда запрос в цикле оправдан или даже необходим. Разберем такие случаи:
Проанализируем пример автора: получение цен по складам. Вместо того чтобы в цикле по складам запрашивать цены, мы можем передать массив складов в один запрос и использовать группировки или временные таблицы.
Рассмотрим пример правильного подхода с использованием временной таблицы:
// 1. Формируем таблицу параметров (например, из ТЗ или массива)
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ_Склады.Склад КАК Склад,
| ТЗ_Склады.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Параметры
|ИЗ
| &ТЗ_Склады КАК ТЗ_Склады
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Склад КАК Склад,
| ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| ВТ_Параметры КАК ВТ_Параметры
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК ЦеныНоменклатурыСрезПоследних
| ПО ВТ_Параметры.Склад = ЦеныНоменклатурыСрезПоследних.Склад
| И ВТ_Параметры.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура";
Запрос.УстановитьПараметр("ТЗ_Склады", ТаблицаСкладовНоменклатуры);
Запрос.УстановитьПараметр("Период", ТекущаяДата());
РезультатЗапроса = Запрос.Выполнить();
В данном примере мы одним «походом» к базе данных получаем все нужные цены. Это работает стабильно как на 5 записях, так и на 5000.
Если по какой-то причине запрос в цикле абсолютно неизбежен, воспользуемся рекомендацией из документации 1С (сообщение 215). Выясним причину: при каждой установке Запрос.Текст = "..." внутри цикла платформа заново анализирует синтаксис строки. Если вынести создание объекта и определение текста за пределы цикла, а внутри менять только параметры, мы сэкономим ресурсы на парсинге.
Сравним два подхода:
Плохо (анализ текста на каждой итерации):
Для Каждого Строка Из СписокСкладов Цикл
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ... ГДЕ Склад = &Склад"; // Парсинг текста 100 раз
Запрос.УстановитьПараметр("Склад", Строка.Склад);
Результат = Запрос.Выполнить();
КонецЦикла;
Чуть лучше (парсинг один раз):
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ... ГДЕ Склад = &Склад"; // Парсинг текста 1 раз
Для Каждого Строка Из СписокСкладов Цикл
Запрос.УстановитьПараметр("Склад", Строка.Склад);
Результат = Запрос.Выполнить(); // Выполнение 100 раз
КонецЦикла;
Хотя второй вариант эффективнее первого, он все равно проигрывает пакетному запросу из-за сетевых задержек (Round-trip), о которых мы говорили выше.
Подведем итог нашему анализу. Запрос в цикле плох не сам по себе, а последствиями, которые он несет для масштабируемости системы. Основные проблемы: высокие накладные расходы на сетевой обмен, многократная проверка RLS и неэффективное использование ресурсов СУБД. Всегда стремитесь перевести алгоритм на рельсы «один запрос — много данных», используя массивы, временные таблицы и соединения. Это гарантирует, что ваш код не станет «узким местом» при росте бизнеса заказчика.