Одной из наиболее распространенных проблем производительности в решениях на платформе 1С:Предприятие является выполнение запросов внутри циклов. В данной статье мы подробно проанализируем, почему такой подход замедляет систему, и разберем на практических примерах, как правильно переписать код с использованием временных таблиц и пакетных запросов.
Проанализируем типичную ситуацию: у нас есть массив ссылок или ТаблицаЗначений с данными, и нам нужно получить дополнительные сведения из базы данных для каждой строки. Новичок часто совершает ошибку, помещая объект Запрос внутрь оператора цикла Для Каждого. Давайте выясним причину, по которой это приводит к фатальному падению производительности.
Каждое выполнение метода Запрос.Выполнить() — это полноценное обращение к серверу базы данных (СУБД). Если в вашей таблице 1000 строк, система выполнит 1000 отдельных сетевых пакетов, 1000 операций открытия и закрытия курсора в SQL. Это создает огромные накладные расходы на сетевой трафик и работу процессора сервера. Рассмотрим эффективную альтернативу: получение всех данных за один раз.
Вместо того чтобы обращаться к базе на каждой итерации, мы сначала соберем все необходимые идентификаторы (например, ссылки на номенклатуру или контрагентов) в одну ТаблицаЗначений. Это позволит нам передать весь список данных в запрос целиком.
Разберем пример создания и наполнения такой таблицы:
// Создаем таблицу для передачи в запрос
ТЗДанных = Новый ТаблицаЗначений;
ТЗДанных.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗДанных.Колонки.Добавить("Склад", Новый ОписаниеТипов("СправочникСсылка.Склады"));
// Допустим, мы заполняем её из какого-то источника
Для Каждого СтрокаИсточника Из ИсточникДанных Цикл
НоваяСтрока = ТЗДанных.Добавить();
НоваяСтрока.Номенклатура = СтрокаИсточника.Номенклатура;
НоваяСтрока.Склад = СтрокаИсточника.Склад;
КонецЦикла;
Важный момент: колонки таблицы значений должны быть типизированы. Если платформа не будет знать тип данных в колонке, передача таблицы в запрос может завершиться ошибкой или привести к неоптимальному плану выполнения на стороне СУБД.
Теперь мы перейдем к самому важному этапу. Мы передадим нашу таблицу ТЗДанных в качестве параметра запроса и поместим её во временную таблицу. Это позволит нам в рамках этого же запроса соединить наши данные с любыми таблицами базы данных (остатками, ценами, реквизитами).
Посмотрим на пример кода, реализующий этот механизм:
Запрос = Новый Запрос;
// Устанавливаем таблицу как параметр
Запрос.УстановитьПараметр("ВнешняяТаблица", ТЗДанных);
// Используем МенеджерВременныхТаблиц для управления жизненным циклом данных
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Склад КАК Склад
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ВнешняяТаблица КАК ВТ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ИсходныеДанные.Номенклатура КАК Номенклатура,
| ВТ_ИсходныеДанные.Склад КАК Склад,
| ЕСТЬNULL(ТоварыНаСкладахОстатки.В НаличииОстаток, 0) КАК Остаток
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(
| &Период,
| Номенклатура В (ВЫБРАТЬ ВТ.Номенклатура ИЗ ВТ_ИсходныеДанные КАК ВТ)) КАК ТоварыНаСкладахОстатки
| ПО ВТ_ИсходныеДанные.Номенклатура = ТоварыНаСкладахОстатки.Номенклатура
| И ВТ_ИсходныеДанные.Склад = ТоварыНаСкладахОстатки.Склад";
РезультатЗапроса = Запрос.Выполнить();
Проанализируем ситуацию: в первом запросе пакета мы создаем временную таблицу ВТ_ИсходныеДанные. Во втором запросе мы выполняем ЛЕВОЕ СОЕДИНЕНИЕ этой временной таблицы с виртуальной таблицей остатков. Это позволяет получить все остатки по списку номенклатуры и складов за один единственный вызов к БД.
Если в вашей временной таблице содержится более нескольких десятков строк, производительность может снизиться из-за отсутствия индексов. В 1С мы можем принудительно проиндексировать поля временной таблицы для ускорения операций соединения.
Рассмотрим, как изменить текст запроса для добавления индексов:
|ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Склад КАК Склад
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ВнешняяТаблица КАК ВТ
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура,
| Склад
|;
Использование ключевого слова ИНДЕКСИРОВАТЬ ПО создаст соответствующие индексы в базе данных tempdb на стороне SQL-сервера. Это критически важно при работе с таблицами объемом в тысячи строк.
После того как мы получили РезультатЗапроса, нам нужно эффективно обработать его. Вместо повторного поиска в таблице значений, мы можем использовать метод Выбрать() или выгрузить результат в новую таблицу для дальнейшего использования.
Разберем пример обхода результата:
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Теперь у нас есть доступ ко всем данным без обращения к базе в цикле
ТекстСообщения = СтрШаблон("Номенклатура: %1, Склад: %2, Остаток: %3",
Выборка.Номенклатура, Выборка.Склад, Выборка.Остаток);
Сообщить(ТекстСообщения);
КонецЦикла;
Подведем итоги нашего анализа и выясним, какие преимущества мы получили, отказавшись от запросов в цикле:
Помните, что использование МенеджерВременныхТаблиц требует контроля памяти. Если вы создаете много временных таблиц, не забывайте очищать менеджер или уничтожать таблицы с помощью конструкции УНИЧТОЖИТЬ в тексте запроса, когда данные в них больше не нужны.
Мы рассмотрели основной механизм оптимизации. Применяйте эти правила в своей ежедневной разработке, и ваши конфигурации на платформе 1С:Предприятие 8.3 будут работать максимально эффективно!