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