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