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

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

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

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

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

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

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

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

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

Математика здесь безжалостна. Если один запрос занимает 5 миллисекунд, то цикл на 10 000 итераций потратит только на обращения к базе около 50 секунд. При этом тот же объём данных один запрос с соединением получил бы за доли секунды, потому что СУБД оптимизирует выборку по всему набору условий сразу.

Дополнительный вред — нагрузка на сервер СУБД. Поток мелких запросов занимает соединения, забивает кэш планов однотипными командами и мешает другим пользователям. В многопользовательской базе обработка с запросом в цикле тормозит не только себя, но и коллег, которые в этот момент проводят документы.

Время растёт пропорционально строкамЛинейная зависимость

Если обработка 100 строк занимает 2 секунды, а 1000 строк — 20 секунд, почти наверняка внутри цикла есть обращение к базе

Монитор СУБД показывает серию одинаковых запросовТысячи однотипных обращений

Откройте активные запросы на сервере СУБД во время выполнения обработки

Замер производительностиМножество вызовов одного запроса

Включите замер производительности в конфигураторе и посмотрите количество вызовов, а не только суммарное время

Как распознать запрос в цикле в коде

Прямой признак — вызов Запрос.Выполнить() или Запрос.Выполнить().Выбрать() внутри конструкций Для Каждого ... Цикл, Пока ... Цикл или Для ... По ... Цикл. Часто параметр запроса при этом устанавливается из текущего элемента цикла: Запрос.УстановитьПараметр("Ссылка", Строка.Номенклатура).

Скрытая форма — когда запрос вынесен в отдельную функцию с красивым названием вроде ПолучитьОстаток(Номенклатура), а эта функция вызывается в цикле. Формально в цикле запроса нет, но по сути это тот же антипаттерн. Проверяйте не только тело цикла, но и все функции, которые из него вызываются.

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

Основные способы убрать запрос из цикла

Один запрос со списком значений

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

Временная таблица и соединение

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

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

Когда логика требует нескольких последовательных запросов (сначала собрать временную таблицу, потом выбрать по ней данные), объедините их в пакетный запрос через разделитель ;. Все запросы пакета выполнятся за одно обращение к серверу, а результаты вернутся массивом через ВыполнитьПакет().

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

Пошаговый алгоритм рефакторинга

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

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

Ключевой приём — выгрузить результат запроса в Соответствие, где ключ — то самое значение, по которому раньше выполнялся запрос (ссылка, код, номенклатура). Тогда внутри цикла получение данных сводится к Соответствие.Получить(Ключ), что выполняется мгновенно и вообще не трогает базу.

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

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

Абсолютный запрет здесь неуместен: есть ситуации, где запрос в цикле — осознанный компромисс. Например, обработка идёт по большой таблице порциями, и на каждую порцию выполняется один запрос. Это уже не антипаттерн, а пакетная обработка: 10 запросов по 1000 строк вместо 10 000 запросов по одной строке.

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

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

Маршрут решения

Как выбрать действие

Условие запроса — одно меняющееся значение (ссылка, код)Соберите значения в массив и используйте оператор В (&Параметр) в одном запросе
Данные для цикла лежат в таблице значений или табличной частиПоместите таблицу во временную таблицу и соедините с таблицами базы
Нужно несколько последовательных выборокОбъедините их в пакетный запрос и выполните через ВыполнитьПакет()
В цикле читаются реквизиты ссылок через точкуВыберите нужные поля одним запросом заранее и обращайтесь к выборке
Итераций гарантированно меньше десятка и список не растётМожно оставить как есть, но добавьте комментарий об ограничении

Типичные ошибки при исправлении

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

Вторая ошибка — забыть про пустой массив параметра. Если до цикла массив значений не заполнился, запрос с условием В (&ПустойМассив) вернёт пустой результат, и обработка молча ничего не сделает. Добавьте проверку: если массив пуст — не выполнять запрос вообще.

Третья ошибка — выгрузить результат в соответствие без учёта дублей ключа. Если запрос возвращает несколько строк на один ключ, Соответствие.Вставить() затрёт предыдущее значение, и часть данных потеряется. Проверяйте кардинальность результата до выбора структуры хранения.

Типичные ошибки
01
Замена запроса на В (&Массив) без учёта того, что условие включало дату или другое поле строки — данные выбираются неверно
02
Запрос с пустым массивом параметра — обработка отрабатывает без ошибок, но ничего не находит
03
Затирание значений в соответствии при нескольких строках на один ключ — теряется часть данных
04
Разыменование ссылок через точку осталось в цикле — запросы убрали, а неявные обращения к базе остались
05
Проверка только на маленьких данных — на тестовых 20 строках разницы нет, проблема всплывает в продуктиве

Как проверить результат после рефакторинга

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

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

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

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

Количество вызовов запроса в замере производительности
Норма

Один или несколько вызовов на всю обработку

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

Число вызовов равно числу обработанных строк

Результат обработки на одинаковых данных
Норма

Полностью совпадает с результатом старой версии

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

Есть расхождения в суммах, количестве или составе строк

Время выполнения на объёме, близком к продуктивному
Норма

Растёт медленнее, чем число строк, и укладывается в приемлемый лимит

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

Время по-прежнему пропорционально строкам

Активность на сервере СУБД во время выполнения
Норма

Единичные тяжёлые запросы

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

Поток тысяч однотипных мелких запросов

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

А если итераций всего пятьдесят, запрос в цикле тоже плохо?

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

Чем заменить обращение к реквизитам ссылки через точку в цикле?

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

Поможет ли вынос запроса в отдельную функцию?

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

Что делать, если запрос в цикле нашёлся в типовом коде конфигурации?

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

Почему запрос с условием В (&Массив) иногда работает медленно?

При очень большом массиве СУБД может построить неоптимальный план. В таких случаях поместите массив во временную таблицу, проиндексируйте её по полю соединения и используйте соединение вместо условия В. Также проверьте, что по полю условия в базе есть подходящий индекс.

Как убедить коллегу, что его код с запросом в цикле нужно переписать?

Самый убедительный аргумент — замер. Включите замер производительности на реальном объёме данных и покажите количество вызовов запроса и долю времени, которую они занимают. Затем продемонстрируйте тот же замер после исправления: разница во времени выполнения обычно снимает все возражения.

Итог

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

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