К статье
Практический материал

Разработка и Конфигурация · 12 мин

Запрос в цикле в 1С: почему это плохо и как правильно переписать код

Разбираем, чем опасен запрос в цикле в 1С: падение производительности, блокировки, нагрузка на СУБД. Показываем, как переписать код на пакетные запросы и временные таблицы.

Запрос в цикле — одна из самых частых причин медленной работы самописных обработок и доработанных конфигураций 1С. Суть антипаттерна проста: внутри цикла обхода таблицы значений, выборки или списка ссылок на каждой итерации выполняется отдельный запрос к базе данных. Код работает «правильно» с точки зрения результата, но катастрофически неэффективно по скорости.

Проблема не в самом цикле, а в количестве обращений к СУБД. Каждый запрос — это передача текста на сервер баз данных, его компиляция, построение плана, выполнение и возврат результата. Если цикл содержит тысячу итераций, вы получаете тысячу отдельных обращений вместо одного. На файловой базе это заметно сразу, на клиент-серверной — проявляется в виде роста нагрузки на SQL Server или PostgreSQL и жалоб пользователей на «тормоза».

Ниже разберём, почему запрос в цикле вреден, как его распознать в чужом и своём коде, и какими способами переписать логику на один-два запроса без потери результата.

Что происходит внутри платформы при запросе в цикле

Каждый вызов метода Выполнить() у объекта «Запрос» запускает полный цикл обработки: платформа передаёт текст на сервер 1С, тот — в СУБД, СУБД разбирает текст, строит или ищет план выполнения, читает данные и возвращает выборку. Даже «лёгкий» запрос к одной таблице тратит время на сетевое взаимодействие и компиляцию.

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

Отдельный вред — частые мелкие транзакции чтения. В клиент-серверном варианте каждое обращение может устанавливать краткоживущие блокировки на читаемые таблицы. При параллельной работе пользователей это повышает вероятность конфликтов и взаимоблокировок, особенно если запрос в цикле выполняется внутри транзакции записи.

СимптомОбработка работает минутами

Проверьте, нет ли внутри цикла вызова Запрос.Выполнить() или методов НайтиПоКоду/НайтиПоНаименованию

СимптомНагрузка на СУБД растёт скачками

В консоли СУБД видно множество однотипных мелких запросов, отличающихся только параметром

СимптомКод содержит «Пока Выборка.Следующий()» и внутри новый запрос

Классический признак: вложенные циклы выборок, где внутренняя выборка зависит от поля внешней

Типичные сценарии, где антипаттерн встречается чаще всего

Чаще всего запрос в цикле появляется не от незнания, а от постепенного разрастания кода: сначала обрабатывалось пять элементов, потом пять тысяч, а структура осталась прежней.

Поиск ссылок по коду или наименованию

Классика загрузки из Excel: для каждой строки файла вызывается Справочники.Номенклатура.НайтиПоКоду(Код) или НайтиПоНаименованию(). Каждый такой вызов — отдельный запрос к базе. При загрузке нескольких тысяч строк время растёт линейно и быстро становится неприемлемым.

Получение остатков или цен для каждой строки таблицы

В цикле по табличной части документа или таблице значений для каждой номенклатуры выполняется запрос остатков по складу или актуальной цены. Правильный подход — один запрос с условием Номенклатура В (&СписокНоменклатуры) или соединением с временной таблицей, а затем поиск нужной строки в выгруженном результате.

Вложенные выборки «шапка — строки»

Сначала выбираются документы за период, затем для каждого документа отдельным запросом вытягиваются его табличные части или связанные данные. Это заменяется одним запросом с соединением таблиц и итогами по полю-ссылке либо обходом с группировкой.

Проверка существования элемента перед созданием

При синхронизации данных для каждой записи источника проверяется «есть ли такой элемент в базе». Вместо тысячи проверочных запросов нужно один раз выбрать все существующие ключи в соответствие и проверять наличие в памяти.

Как переписать код: основные приёмы

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

Условие «В» по списку значений

Если в цикле ищутся данные по одному ключу (код, артикул, ссылка), соберите все ключи в массив и передайте его параметром. Запрос вида ВЫБРАТЬ Ссылка, Код ИЗ Справочник.Номенклатура ГДЕ Код В (&Коды) вернёт все найденные элементы за одно обращение. Результат выгрузите в таблицу значений или соответствие для быстрого поиска.

Временные таблицы

Когда входных данных много и они содержат несколько полей (например, номенклатура + характеристика + склад), поместите таблицу значений во временную таблицу через менеджер временных таблиц и соедините её с реальными таблицами базы. Это самый гибкий способ: СУБД сама построит эффективное соединение.

Запрос = Новый Запрос;

Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;

Запрос.Текст =

"ВЫБРАТЬ Т.Номенклатура, Т.Склад

|ПОМЕСТИТЬ ВТ_Товары

|ИЗ &ТаблицаТоваров КАК Т

|;

|ВЫБРАТЬ ВТ.Номенклатура, Остатки.КоличествоОстаток

|ИЗ ВТ_Товары КАК ВТ

|ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки

|ПО ВТ.Номенклатура = Остатки.Номенклатура И ВТ.Склад = Остатки.Склад";

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

Результат = Запрос.Выполнить().Выгрузить();

Пакетные запросы

Если данных много и логика состоит из нескольких этапов, объедините их в пакетный запрос: несколько операторов ВЫБРАТЬ в одном тексте, разделённые точкой с запятой. Все они выполнятся за одно обращение к серверу, а промежуточные результаты сохранятся во временных таблицах.

Соответствие вместо повторного поиска

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

Порядок рефакторинга
  1. 1
    Найдите в цикле вызовы Запрос.Выполнить(), НайтиПоКоду(), НайтиПоНаименованию(), ПолучитьОбъект() с последующим чтением реквизитов
  2. 2
    Определите, какие данные реально нужны на каждой итерации — обычно это 2–5 полей
  3. 3
    Соберите входные ключи цикла в массив или таблицу значений до начала цикла
  4. 4
    Напишите один запрос с условием В или соединением с временной таблицей
  5. 5
    Выгрузите результат и постройте соответствие для поиска по ключу
  6. 6
    В цикле замените обращение к базе на обращение к соответствию
  7. 7
    Замерьте время выполнения до и после, чтобы зафиксировать результат

Как проверить, что стало лучше

Главный критерий — количество обращений к СУБД. До рефакторинга их столько, сколько итераций в цикле; после — одно или несколько, не зависящих от объёма данных. Время выполнения при росте объёма должно расти умеренно, а не линейно «взрываться».

Для замера используйте замер времени через ТекущаяУниверсальнаяДатаВМиллисекундах() до и после фрагмента кода. В клиент-серверной базе полезно посмотреть технологический журнал с фильтром по событию DBMSSQL/DBPOSTGRS: до правки вы увидите тысячи однотипных запросов, после — единичные.

Также проверьте корректность результата: сравните итоговые данные старой и новой версии обработки на одном и том же наборе входных данных. Оптимизация не должна менять бизнес-логику.

Контрольные точки

Как проверить результат

Количество запросов к СУБД
Норма

Один или несколько запросов независимо от числа строк

Тревожный признак

Число запросов равно числу итераций цикла

Время выполнения при росте объёма в 10 раз
Норма

Время растёт умеренно

Тревожный признак

Время выросло примерно в 10 раз

Результат обработки
Норма

Данные совпадают со старой версией на тестовом наборе

Тревожный признак

Есть расхождения — ошибка в соединении или условии

Поведение параллельных пользователей
Норма

Жалоб на блокировки нет

Тревожный признак

Появились ошибки превышения времени ожидания блокировки

Когда запрос в цикле допустим

Абсолютных запретов нет: если цикл гарантированно короткий (единицы элементов) и выполняется редко, отдельные запросы не нанесут заметного вреда. Например, обработка, которая раз в день обрабатывает пять документов, не требует сложной оптимизации.

Также бывают ситуации, когда объединить данные в один запрос невозможно из-за разной логики выборки на каждой итерации. В таком случае старайтесь хотя бы сократить число итераций, вынести инвариантные части за цикл и переиспользовать один объект «Запрос» сменой параметров — это дешевле, чем создавать новый запрос каждый раз.

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

Типичные ошибки при оптимизации

Первая ошибка — заменить запрос в цикле на обращение к реквизитам через точку от ссылки в цикле. Конструкция вида Строка.Номенклатура.Наименование внутри цикла порождает те же скрытые обращения к базе: платформа дочитывает объект по ссылке. Это тот же антипаттерн, только менее заметный.

Вторая ошибка — выбирать «всё про всё» одним гигантским запросом без условий и потом фильтровать в памяти. Если таблица содержит миллионы записей, такой запрос может быть хуже исходного кода. Всегда ограничивайте выборку периодом, списком ключей или временной таблицей.

Третья ошибка — забыть про индексы. Соединение временной таблицы с регистром по неиндексированным полям может дать полное сканирование. Проверяйте, что поля соединения входят в индексы таблиц или в измерения регистра.

Типичные ошибки
01
Обращение к реквизитам ссылки через точку в цикле — скрытые запросы к базе вместо явных
02
Выборка всей таблицы без отбора с последующей фильтрацией в памяти — перегрузка и СУБД, и клиента
03
Соединение временной таблицы по неиндексированным полям — полное сканирование регистра
04
Забыть сравнить результат новой и старой версии — оптимизация молча меняет данные
05
Оптимизировать код, который выполняется раз в месяц над пятью элементами — потраченное время не окупается

Частые вопросы

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

На тестовой базе мало данных, короткие таблицы и нет параллельных пользователей, поэтому накладные расходы на каждое обращение незаметны. На рабочей базе растут объёмы таблиц, добавляется конкуренция за блокировки и сетевая задержка между сервером 1С и СУБД — и линейный рост числа запросов превращается в проблему.

Чем плохи методы НайтиПоКоду и НайтиПоНаименованию в цикле?

Каждый вызов этих методов — отдельный запрос к базе данных. При обходе тысячи строк загружаемого файла получается тысяча запросов. Замените их одним запросом с условием «Код В (&МассивКодов)» и поиском по выгруженному результату.

Что лучше: условие «В» с массивом или временная таблица?

Для одного простого ключа (код, ссылка) достаточно условия «В» с массивом — это короче и проще. Временная таблица нужна, когда ключ составной (несколько полей), когда данных очень много или когда таблица участвует в нескольких запросах пакета.

Можно ли оставить запрос в цикле, если итераций всегда меньше десяти?

Да, при гарантированно малом и контролируемом числе итераций это допустимо и не даст заметной нагрузки. Но убедитесь, что ограничение действительно гарантировано логикой, а не «пока так получается» — объёмы данных имеют свойство расти.

Как найти все места с запросом в цикле в большой конфигурации?

Используйте глобальный поиск по конфигуратору: ищите вхождения «Выполнить()», «НайтиПоКоду», «НайтиПоНаименованию» и проверяйте, не находятся ли они внутри конструкций «Пока... Цикл» или «Для Каждого... Цикл». Дополнительно полезен анализ технологического журнала на предмет массовых однотипных запросов.

Поможет ли переход с файловой базы на клиент-серверную?

Нет, наоборот: в клиент-серверном варианте добавляется сетевая задержка на каждое обращение, и запрос в цикле может стать ещё заметнее. Смена архитектуры не лечит алгоритмические ошибки — сначала нужно убрать множественные обращения к базе.

Итог

Что важно запомнить

Запрос в цикле умножает накладные расходы на число итераций и является главной причиной медленных обработок в 1С. Решение всегда одно: собрать входные данные, получить всё одним запросом (условие «В», временные таблицы, пакетный запрос) и работать с результатом в памяти через соответствие. Перед внедрением проверьте совпадение результата со старой версией и замерьте время.