Для каждого СтрокаТовары из Товары Цикл
//.....................
Количество = СтрокаТовары.Количество * СтрокаТовары.Коэффициент / СтрокаТовары.Номенклатура.ЕдиницаХраненияОстатков.Коэффициент;
//......................
КонецЦикла;
В чем проблема?
Когда мы обращаемя к реквизиту объекта базы "через точку", программа выполняет запрос к базе, чтобы вытащить из базы значение этого реквизита. Строго говоря, это происходит не всегда. Известно, что при чтении одного реквизита считываются значения всех реквизитов этого объекта. Считанные значения кэшируется, но кэш имеет ограниченный размер, и ограниченное время жизни.
Такой запрос выполняется довольно быстро, и обычно не дает заметной задержки. Задрежку, заметную для пользователя, можно получить, если в цикле будет более 100 таких опеаций. Задержки на выполнении запросов более заметны в клиент-серверном режиме, чем в файл-серверном. Зачастую, после перевода файловой базы, которая работала в терминале, на SQL пользователи начинаются жаловаться на то, что программа стала тормозить. Проблема может быть как раз в этих микрозапросах, которые выполяются в цикле. Судя по всему, задержка на выполнение такого микрозапроса складывается в основном из накладных расходов: нужно сформировать текст запроса, отправить его на сервер, откомпилировать, построить план получить результат и т.д. Я могу ошибаться в том, откуда эти накладные расходы берутся, но они есть, в этом я не раз убеждался на практике.
Кэширование, которое выполняет платформа, не всегда спасает: если в цикле каждый раз считывается новая номенклатура, то ее еще нет в кэше, и мы каждый раз несем те самые накладные расходы.
Я лично сталкивался с ситуацией, когда такой вот неаккуратный код замедлял проведение размещение заказа покупателя в 10 раз (справедливости разди, в заказе было 800 строк).
Естественно, нет необходимости переписывать все такие случаи. Если есть потребность оптимизировать какую-то операцию, то надо сначала сделать замер производительности (такая функция есть в конфигураторе, в меню "Отладка"). В нашем случае, мы с топе увидим простую операцию, которая выполняется многократно и съедает много времени. Не забудьте, что замер надо делать на клиент-серверной базе, а не на файловой копии.
Исправляем
Т.к. проблема в накладных расходах, то считав все необходимые данные одним запросом, мы многократно снизим накладные расходы и решим проблему. Первое что приходит в голову, это написать что-то наподобие такого кода:
//....................
Запрос.Текст =
//............
|НакладнаяТовары.Количество КАК Количество,
|НакладнаяТовары.Коэффициент КАК Коэффициент,
|НакладнаяТовары.Номенклатура.БазоваяЕдиницаИзмерения.Коэффициент КАК НоменклатураБазоваяЕдиницаИзмеренияКоэффициент,
//.........
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
//.................
Количество = Выборка.Количество*Выборка.Коэффициент*Выборка.НоменклатураБазоваяЕдиницаИзмеренияКоэффициент;
//.................
КонецЦикла;
Это решит проблему, только мы можем столкнуться с некоторыми неудобствами. Во-первых нам придется везде заменить СтрокаТовары на Выборка. Хорошо, обзовем выборку "СтрокаТовары" (что не очень красиво) или выгрузим запрос в таблицу значений (что уже лучше).
Во-вторых, если запрос сложный, собирается по-частям где-то в общих модулях типовой конфигурации, то мы не захотим его менять. Потому что боимся что-нибудь поломать, а еще не хотим усложнять себе обновление типовой конфигурации с сохранением сделанных настроек.
Поразмыслив, пишем более приятный и удобный вариант:
Запрос.Текст =
"ВЫБРАТЬ
|Номенклатура.Ссылка,
|Номенклатура.ЕдиницаХраненияОстатков.Коэффициент КАК ЕдиницаХраненияОстатковКоэффициент
|ИЗ
|Справочник.Номенклатура КАК Номенклатура
|ГДЕ
|Номенклатура.Ссылка В (&МассивНоменклатуры)";
Запрос.Параметры.Вставить("МассивНоменклатуры", Товары.ВыгрузитьКолонку("Номенклатура"));
тзРеквизитовНоменклатуры = Запрос.Выполнить().Выгрузить();
тзРеквизитовНоменклатура.Индексы.Добавить("Ссылка");
Для каждого СтрокаТовары из Товары Цикл
СтрокаНоменклатуры = тзРеквизитовНоменклатуры.Найти(СтрокаТовары.Номенклатура, "Ссылка");
Если СтрокаНоменклатуры = Неопределено Тогда
ВызватьИсключение "Не найдена номенклатура"+СтрокаТовары.Номенклатура;
КонецЕсли;
//...................
Количество = СтрокаТовары.Количество*СтрокаТовары.Коэффициент*СтрокаНоменклатуры.ЕдиницаХраненияОстатковКоэффициент;
//....................
КонецЦикла;
Мы внесли в код локальные измененения:
- Перед циклом вытащили из базы то, что нам нужно, и сложили в таблицу значений.
- В начале цикла нашли в этой таблице нужную строку.
- Везде, где есть обращение к полям "через точку", заменили на обращение к строке таблицы значений.
Необходимость добавлять индекс в таблицу значений - вопрос спорный, я рассуждаю так:
- Индексирование маленькой тз не даст заметной задержки. Даже если мы потеряем больше времени, чем сэкономим - мы не гонимся за рекордами, нам важно, чтобы пользователю было комфортно.
- Отсутствие индекса в большой тз может дать заметную задержку. Справедливости ради, таблица должна быть действительно большой, начиная с 1000 записей, если я ничего не путаю.
Немного сложнее
Бывает, что неявный микрозапрос, который многократно вызывается, закопан глубоко в общих модулях, да еще используется по всей конфигурации. Что делать в этом случае? Рассмотрим пример:
Функция ПолучитьКурсВалюты(Валюта, ДатаКурса) Экспорт
Структура = РегистрыСведений.КурсыВалют.ПолучитьПоследнее(?(ДатаКурса = Дата('00010101'),ТекущаяДата(),ДатаКурса), Новый Структура("Валюта", Валюта));
Возврат Структура;
КонецФункции // ПолучитьКурсВалюты()
Если многократный вызов этой функции съедает время наших пользователей, мы можем ее усовершенствовать:
Функция ПолучитьКурсВалюты(Валюта, ДатаКурса, СтруктураКэш = Неопределено) Экспорт
// добавили тут
лкДата = ?(ДатаКурса = Дата('00010101'),ТекущаяДата(),ДатаКурса);
Если СтруктураКэш <> Неопределено Тогда
Если НЕ СтруктураКэш.Свойство("КэшКурсов") Тогда
СтруктураКэш.Вставить("КэшКурсов", Новый ТаблицаЗначений);
СтруктураКэш.КэшКурсов.Колонки.Добавить("Валюта");
СтруктураКэш.КэшКурсов.Колонки.Добавить("Период");
СтруктураКэш.КэшКурсов.Колонки.Добавить("Курс");
СтруктураКэш.КэшКурсов.Колонки.Добавить("Кратность");
КонецЕсли;
СтрокиКэш = СтруктураКэш.КэшКурсов.НайтиСтроки(Новый Структура("Валюта, Период", Валюта, лкДата));
Если СтрокиКэш.Количество() > 0 Тогда
Возврат Новый Структура("Курс, Кратность", СтрокиКэш[0].Курс, СтрокиКэш[0].Кратность);
КонецЕсли;
КонецЕсли;
Структура = РегистрыСведений.КурсыВалют.ПолучитьПоследнее(лкДата, Новый Структура("Валюта", Валюта));
// и тут
Если СтруктураКэш <> Неопределено Тогда
НоваяСтрока = СтруктураКэш.КэшКурсов.Добавить();
НоваяСтрока.Валюта = Валюта;
НоваяСтрока.Период = лкДата;
НоваяСтрока.Курс = Структура.Курс;
НоваяСтрока.Кратность = Структура.Кратность;
КонецЕсли;
Возврат Структура;
КонецФункции // ПолучитьКурсВалюты()
Теперь нам надо объявить переменную СтруктураКэш в модуле того объекта, который оптимизируем. Иницилизировать ее не надо, пусть будет Неопределено. Будем передавать эту переменную во все процедуры, которые в конечном итоге вызывают ПолучитьКурсВалюты() - отладчик, замер производительности и стек вызовов в помощь. Придется добавить в типовые процедуры параметр СтруктураКэш = Неопределено, ничего не поделаешь. Но в общем и целом изменений в типовом коде получится немного, а не затронутые нами участки типового кода будут работать по-старому.
Вывод
Оптимизация работы системы не всегда требует чего-то сложного, наподобие анализа блокировок или планов запроса. Часто простой замер производительности и чистка запросов в цикле дает более, чем ощутимый результат.