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