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