При обработке и заполнении табличных частей документов одной из самых частых причин низкой производительности является вызов запросов внутри цикла. Если таблица содержит сотни строк, а обработка выполняется за каждый день недели отдельно, программа делает тысячи лишних обращений к базе данных. В этой статье мы разберем, как кардинально ускорить работу кода и оптимизировать алгоритм заполнения.
Рассмотрим распространенную ситуацию: необходимо заполнить табличную часть документа продажами за каждый день прошедшей недели. Часто разработчики совершают ошибку, выполняя запрос к регистру накопления на каждой итерации цикла по номенклатуре и по дням.
Выполнение запросов в цикле создает следующие проблемы:
Разберем базовый способ оптимизации. Сначала мы собираем массив всей номенклатуры из табличной части документа, передаем его в параметр запроса и получаем обороты за весь период недели за один раз. Затем выгружаем результат в таблицу значений и в цикле ищем нужные строки.
Посмотрим на пример реализации данного подхода:
Процедура Заполнить(Документ)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПродажиОбороты.Номенклатура КАК Номенклатура,
| ПродажиОбороты.КоличествоОборот КАК Количество,
| ПродажиОбороты.СтоимостьОборот КАК Сумма,
| ПродажиОбороты.Период КАК Период
|ИЗ
| РегистрНакопления.Продажи.Обороты(
| &ДатаНач,
| &ДатаКон,
| День,
| Номенклатура В (&МассивНоменклатуры)
| И Подразделение = &Подразделение) КАК ПродажиОбороты";
Запрос.УстановитьПараметр("Подразделение", Документ.Подразделение);
Запрос.УстановитьПараметр("ДатаНач", НачалоДня(Документ.НачНедели));
Запрос.УстановитьПараметр("ДатаКон", КонецДня(НачалоДня(Документ.НачНедели) + 6 * 86400));
Запрос.УстановитьПараметр("МассивНоменклатуры", Документ.Товары.ВыгрузитьКолонку("Номенклатура"));
ТЗ = Запрос.Выполнить().Выгрузить();
// Для ускорения поиска в памяти создаем индекс
ТЗ.Индексы.Добавить("Номенклатура, Период");
Отбор = Новый Структура("Номенклатура, Период");
Для Каждого Стр Из Документ.Товары Цикл
ТекДень = НачалоДня(Документ.НачНедели);
ОбщееКоличество = 0;
ОбщаяСумма = 0;
Для сч = 1 По 7 Цикл
Отбор.Номенклатура = Стр.Номенклатура;
Отбор.Период = ТекДень;
СтрокиТЗ = ТЗ.НайтиСтроки(Отбор);
Если СтрокиТЗ.Количество() Тогда
Выборка = СтрокиТЗ[0];
Количество = Выборка.Количество;
Сумма = Выборка.Сумма;
Стр["ДеньНедели" + сч] = Количество;
Стр["ЦенаДень" + сч] = Сумма;
ОбщееКоличество = ОбщееКоличество + Количество;
ОбщаяСумма = ОбщаяСумма + Сумма;
КонецЕсли;
ТекДень = ТекДень + 86400;
КонецЦикла;
Если ОбщаяСумма <> 0 И ОбщееКоличество <> 0 Тогда
Стр.СредняяЦена = ОбщаяСумма / ОбщееКоличество;
КонецЕсли;
Стр.ОстатокПоСистеме = Стр.Отправлено - ОбщееКоличество;
Стр.Продано = ОбщееКоличество;
КонецЦикла;
Документ.Товары.Сортировать("Номенклатура");
КонецПроцедуры
Для ускорения поиска в полученной таблице значений крайне важно использовать индексацию. Вызов метода ТЗ.Индексы.Добавить("Номенклатура, Период") переводит поиск с помощью НайтиСтроки() из линейного $O(N)$ в быстрый бинарный поиск.
Еще более эффективный подход — переложить всю работу по группировке и распределению продаж по дням недели на сервер СУБД. С помощью конструкции ВЫБОР КОГДА мы можем развернуть дни недели в отдельные колонки непосредственно в запросе.
Проанализируем пример такого запроса:
ВЫБРАТЬ
ПродажиОбороты.Номенклатура КАК Номенклатура,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 2 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели1,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 3 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели2,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 4 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели3,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 5 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели4,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 6 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели5,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 7 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели6,
СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 1 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели7,
СУММА(ПродажиОбороты.КоличествоОборот) КАК ОбщееКоличество,
СУММА(ПродажиОбороты.СтоимостьОборот) КАК ОбщаяСумма
ИЗ
РегистрНакопления.Продажи.Обороты(
&ДатаНач,
&ДатаКон,
День,
Номенклатура В (&МассивНоменклатуры)
И Подразделение = &Подразделение) КАК ПродажиОбороты
СГРУППИРОВАТЬ ПО
ПродажиОбороты.Номенклатура
При таком подходе результат запроса содержит сразу готовую структуру по каждой номенклатуре. Нам остается лишь обойти выборку и применить функцию ЗаполнитьЗначенияСвойств() для переноса данных в строки табличной части.
МенеджерВременныхТаблиц и индексировать поле Номенклатура.Стр.Номенклатура.Артикул) генерируются скрытые запросы к базе данных. Выбирайте все необходимые реквизиты сразу в основном запросе.ЗаполнитьЗначенияСвойств(СтрТабличнойЧасти, Выборка) ускоряет работу интерпретатора 1С и избавляет от громоздких условий Если ... ИначеЕсли.