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