Почему нельзя использовать запросы и чтение наборов записей в цикле в 1С и как правильно оптимизировать такой код?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Конструкция «запрос в цикле» — один из самых частых антипаттернов в разработке на платформе 1С:Предприятие. Подобный подход прямо нарушает официальные стандарты разработки фирмы «1С» и при росте объема данных неминуемо приводит к деградации производительности всей информационной базы. Разберем по шагам, почему так писать нельзя, к каким последствиям для СУБД это приводит и как правильно переписать алгоритм на пакетную обработку.

Анализ проблемы: почему запрос в цикле считается грубой ошибкой

Проанализируем техническую сторону выполнения кода, когда запрос или обращение к данным помещаются внутрь итератора. Основные причины замедления работы кроются в архитектуре взаимодействия компонентов платформы:

  1. Множественные сетевые задержки (Network Round-trips): при каждом выполнении метода Выполнить() или Прочитать() сервер приложений 1С отправляет отдельный сетевой пакет серверу СУБД и ожидает от него ответа. Даже если сам запрос выполняется за доли миллисекунды, накладные расходы на сетевой обмен при сотнях и тысячах итераций занимают львиную долю общего времени.
  2. Накладные расходы механизма SDBL: язык запросов 1С транслируется платформой во внутренний диалект конкретной СУБД (Microsoft SQL Server, PostgreSQL, Oracle и др.). Создание нового объекта Запрос и разбор текста внутри цикла заставляют платформу многократно повторять парсинг и валидацию прав доступа.
  3. Нерациональное использование кэша планов запросов СУБД: непрерывная отправка однотипных мелких запросов создает паразитическую нагрузку на процессор СУБД и засоряет кэш планов выполнения (Plan Cache Pollution).
  4. Блокировки и накладные расходы объектных методов: создание набора записей через СоздатьНаборЗаписей() и его метод Прочитать() для каждой строки цикла инициализирует полные объектные структуры платформы, что расходует оперативную память сервера приложений.

Разбор исходного неоптимального кода

Посмотрим на пример из реальной практики, где в цикле регламентного задания обрабатываются строки поступлений, проверяется наличие открытых форм через регистр сведений и выполняется поиск ссылки на документ:


Для Каждого ТекСтрТаб Из СтрокиПоступлений Цикл 
    Сообщить("Обработка поступления " + ТекСтрТаб.Файл.Имя + " .");    
    НаборЗаписейОткрытыхФорм = РегистрыСведений.ОткрытыеФормыДокументов.СоздатьНаборЗаписей();
    НаборЗаписейОткрытыхФорм.Отбор.ДокументЗН.Установить(ТекСтрТаб.ЗН);
    НаборЗаписейОткрытыхФорм.Прочитать();
    
    Если НаборЗаписейОткрытыхФорм.Количество() = 0 Тогда
        ПоступлениеРанее = НайтиДокументПоступлениеТМЦ(ТекСтрТаб.НомерДокументаПоступления, ТекСтрТаб.ДатаВходящегоДокумента);
        // Дальнейшая обработка документа...
    Иначе 
        // Есть незакрытые формы данного документа, документ не перемещается
    КонецЕсли;    
КонецЦикла;

Функция НайтиДокументПоступлениеТМЦ(ДокНомер, ДокДата)
    ЗапросДок = Новый Запрос;
    ЗапросДок.Текст =  
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |    ПоступлениеТМЦ.Ссылка
        |ИЗ
        |    Документ.ПоступлениеТМЦ КАК ПоступлениеТМЦ
        |ГДЕ
        |    ПоступлениеТМЦ.Номер = &Номер
        |    И ПоступлениеТМЦ.Дата = &Дата";
        
    ЗапросДок.УстановитьПараметр("Номер", ДокНомер);
    ЗапросДок.УстановитьПараметр("Дата", ДокДата);
    
    ВыборкаДок = ЗапросДок.Выполнить().Выгрузить();
    Если ВыборкаДок.Количество() > 0 Тогда              
        Возврат ВыборкаДок[0].Ссылка;
    Иначе 
        Возврат Документы.ПоступлениеТМЦ.ПустаяСсылка();
    КонецЕсли;            
КонецФункции

В данном фрагменте на каждой итерации происходят сразу две критические ошибки: чтение набора записей регистра и вызов отдельного запроса к таблице документов. Если в таблице СтрокиПоступлений будет даже 50 строк, сервер выполнит 100 обращений к базе данных вместо одного.

Пошаговая оптимизация: использование одного пакетного запроса и временных таблиц

Рассмотрим, как преобразовать такой код в правильный, производительный вариант. Главное правило рефакторинга: все проверки и соединения данных должны выполняться на стороне СУБД в рамках одного запроса до входа в цикл обработки.

Разберем этапы оптимизации:

  1. Подготавливаем исходную таблицу значений СтрокиПоступлений и передаем её в качестве параметра в запрос.
  2. Во временной таблице сохраняем входящие данные.
  3. Выполняем левое соединение с таблицей регистра сведений РегистрСведений.ОткрытыеФормыДокументов для проверки блокировок (условие ЕСТЬ NULL исключает заблокированные строки).
  4. Выполняем левое соединение с таблицей документов Документ.ПоступлениеТМЦ по полям Номер и Дата для получения ссылки.
  5. Обходим результат запроса последовательно через метод Выбрать() без лишней выгрузки в промежуточную таблицу значений.

Посмотрим на оптимизированный пример реализации:


// Формируем единый запрос для всей коллекции данных
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ВхДанные.ФайлИмя КАК ФайлИмя,
    |    ВхДанные.ЗН КАК ЗН,
    |    ВхДанные.НомерДокументаПоступления КАК Номер,
    |    ВхДанные.ДатаВходящегоДокумента КАК Дата
    |ПОМЕСТИТЬ втВходныеДанные
    |ИЗ
    |    &ТаблицаПоступлений КАК ВхДанные
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    вт.ФайлИмя КАК ФайлИмя,
    |    вт.ЗН КАК ЗН,
    |    ЕСТЬNULL(ПоступлениеТМЦ.Ссылка, ЗНАЧЕНИЕ(Документ.ПоступлениеТМЦ.ПустаяСсылка)) КАК ПоступлениеРанее
    |ИЗ
    |    втВходныеДанные КАК вт
    |    // Проверяем наличие открытых форм через левое соединение
    |    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОткрытыеФормыДокументов КАК ОткрытыеФормы
    |        ПО вт.ЗН = ОткрытыеФормы.ДокументЗН
    |    // Соединяем с таблицей документов по номеру и дате
    |    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТМЦ КАК ПоступлениеТМЦ
    |        ПО вт.Номер = ПоступлениеТМЦ.Номер
    |        И вт.Дата = ПоступлениеТМЦ.Дата
    |ГДЕ
    |    ОткрытыеФормы.ДокументЗН ЕСТЬ NULL"; // Отсекаем заблокированные документы

Запрос.УстановитьПараметр("ТаблицаПоступлений", СтрокиПоступлений);

Выборка = Запрос.Выполнить().Выбрать();

// В цикле выполняем только чистую бизнес-логику без обращений к БД
Пока Выборка.Следующий() Цикл
    ПоступлениеРанее = Выборка.ПоступлениеРанее;
    // Выполняем дальнейшую обработку документа...
КонецЦикла;

Особенности поиска документов по номеру и дате

Обратим внимание на важную деталь при поиске документов по дате. В платформе 1С стандартный индекс для документов чаще всего строится по полям ПериодНомера + Номер или Дата + Номер. Если входящая дата не содержит времени (равна началу дня 00:00:00), а реальные документы записаны с точным временем, точное условие равенства ПоступлениеТМЦ.Дата = вт.Дата может не найти документ.

Если дата входящего документа задана без времени, соединение в запросе следует строить по периоду:


ПО вт.Номер = ПоступлениеТМЦ.Номер
И ПоступлениеТМЦ.Дата МЕЖДУ вт.Дата И КОНЕЦПЕРИОДА(вт.Дата, ДЕНЬ)

Если же требуется точечный поиск одиночного документа в прикладном коде вне пакетной обработки, предпочтительнее использовать встроенный платформенный метод менеджера документа НайтиПоНомеру(Номер, Дата), так как он использует оптимизированный внутренний индекс платформы.

Инструменты контроля и доказательства неэффективности

Если требуется аргументированно продемонстрировать коллегам разницу в производительности, используем следующие инструменты:

  1. Замер производительности в конфигураторе: покажет суммарное процессорное время выполнения функций и строк с методом Выполнить().
  2. Технологический журнал (ТЖ): сбор событий DBMSSQL / DBPOSTGRS с фильтром по контексту модуля зафиксирует точное число вызовов к СУБД (кол-во строк лога наглядно покажет сотни запросов вместо одного).
  3. Автоматизированные проверки: статический анализ кода (встроенная проверка конфигурации, SonarQube / 1C:Enterprise Development Tools) автоматически помечает запросы в цикле как критические дефекты качества кода.
← На главную