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

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

1С: как обойти запрос в цикле и ускорить обработку данных

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

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

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

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

Почему запрос в цикле — это проблема

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

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

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

СимптомОбработка или проведение «зависает» на больших объёмах

Проверьте, растёт ли время выполнения пропорционально числу строк

Признак в кодеЗапрос.Выполнить() внутри Пока Цикл или Для Каждого

Найдите все вызовы Выполнить() и проверьте контекст

Нагрузка на СУБДМного однотипных мелких запросов в мониторе

Посмотрите технологический журнал или средства мониторинга СУБД

Базовый приём: параметр-массив и оператор В

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

Оператор В в языке запросов принимает список значений, включая массив. Запрос выполняется один раз, возвращает все нужные данные, а в цикле остаётся только поиск по выгруженному результату.

МассивНоменклатуры = Новый Массив;

Для Каждого СтрокаТовара Из Товары Цикл

МассивНоменклатуры.Добавить(СтрокаТовара.Номенклатура);

КонецЦикла;

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

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

"ВЫБРАТЬ

| ТоварыНаСкладах.Номенклатура КАК Номенклатура,

| ТоварыНаСкладах.КоличествоОстаток КАК Остаток

|ИЗ

| РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (&МассивНоменклатуры)) КАК ТоварыНаСкладах";

Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);

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

Дальше в цикле вместо запроса используется поиск по выгруженной таблице. Чтобы поиск не стал новым узким местом, добавьте индекс: метод Добавить() у коллекции индексов таблицы значений. Искать по индексированной колонке методом Найти() — это операция близкая к константному времени.

Результат.Индексы.Добавить("Номенклатура");

Для Каждого СтрокаТовара Из Товары Цикл

НайденнаяСтрока = Результат.Найти(СтрокаТовара.Номенклатура, "Номенклатура");

Если НайденнаяСтрока <> Неопределено Тогда

// работаем с остатком

КонецЕсли;

КонецЦикла;

Временные таблицы для сложных условий

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

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

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

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

"ВЫБРАТЬ

| ТЗ.Номенклатура КАК Номенклатура,

| ТЗ.Склад КАК Склад

|ПОМЕСТИТЬ ВТ_Отбор

|ИЗ

| &ТаблицаОтбора КАК ТЗ

|;

|ВЫБРАТЬ

| ВТ_Отбор.Номенклатура,

| ВТ_Отбор.Склад,

| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток

|ИЗ

| ВТ_Отбор КАК ВТ_Отбор

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

| ПО ВТ_Отбор.Номенклатура = Остатки.Номенклатура

| И ВТ_Отбор.Склад = Остатки.Склад";

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

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

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

Соединение с таблицей документа вместо обхода строк

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

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

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

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

Соответствие как кэш для повторных обращений

Иногда запрос в цикле появляется неосознанно: разработчик вызывает функцию, которая внутри себя выполняет запрос, и не замечает этого. Типичные примеры — функции получения ставки НДС, курса валюты, признака ведения учёта по характеристикам. Если такая функция вызывается в цикле, проблема та же, просто спрятанная глубже.

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

КэшСтавок = Новый Соответствие;

Для Каждого СтрокаТовара Из Товары Цикл

Ставка = КэшСтавок.Получить(СтрокаТовара.Номенклатура);

Если Ставка = Неопределено Тогда

Ставка = ПолучитьСтавкуНДС(СтрокаТовара.Номенклатура); // здесь запрос

КэшСтавок.Вставить(СтрокаТовара.Номенклатура, Ставка);

КонецЕсли;

КонецЦикла;

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

Сравнение подходов

Массив + оператор В

Подходит для отбора по одному полю. Просто и быстро, но не работает с составными условиями

Временная таблица из ТЗ

Универсальный вариант для составных отборов и соединений. Требует типизированных колонок и контроля дублей

Кэш-соответствие

Быстрая доработка при малом числе уникальных значений. Не решает проблему при большом разнообразии ключей

СКД / один большой запрос

Лучший вариант для отчётов: вся выборка и группировки на стороне СУБД. Требует переработки логики

Запросы в цикле при обходе результата запроса

Отдельный антипаттерн — вложенные запросы при групповом обходе: внешний запрос выбирает документы, а для каждого документа в цикле выполняется запрос по его табличной части или связанным данным. Это классическая проблема «N+1 запросов».

Решение — один запрос с соединением: выбираются документы и сразу соединяются с нужными данными по ссылке. Если нужна иерархия «документ — строки», используйте итоги: выберите строки с указанием ИТОГИ ПО Ссылка, и обход станет двухуровневым без единого дополнительного запроса.

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

"ВЫБРАТЬ

| ТабЧасть.Ссылка КАК Документ,

| ТабЧасть.Номенклатура КАК Номенклатура,

| ТабЧасть.Количество КАК Количество

|ИЗ

| Документ.РеализацияТоваровУслуг.Товары КАК ТабЧасть

|ГДЕ

| ТабЧасть.Ссылка.Дата МЕЖДУ &Начало И &Конец

|ИТОГИ ПО

| ТабЧасть.Ссылка";

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

Проверка результата и типичные ошибки

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

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

Типичные ошибки при оптимизации
01
Передача таблицы с дублями во временную таблицу — соединение размножает строки, итоги завышаются
02
Внутреннее соединение вместо левого — строки без остатков пропадают из результата
03
Поиск по неиндексированной таблице в цикле — Найти() перебирает все строки, и выигрыш от одного запроса съедается медленным поиском
04
Кэш без учёта периода или отбора — функция возвращает устаревшие значения при смене контекста
05
Обращение к реквизитам через точку в цикле (Строка.Номенклатура.СтавкаНДС) — это тоже скрытые обращения к базе, хоть и не явный запрос

Отдельно упомянем обращения через точку от ссылки. Конструкция вида СтрокаТовара.Номенклатура.Наименование в цикле не выглядит как запрос, но фактически каждое такое обращение к реквизиту ссылки инициирует чтение объекта из базы. Лечится тем же способом: нужные реквизиты выбираются в запросе сразу, а в цикле берутся из полей выборки.

Итог

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

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

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

А если в цикле нужно не читать, а записывать данные — это тоже плохо?

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

Есть ли ограничение на размер массива в операторе В?

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

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

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

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

Если данные берутся из выборки запроса — добавьте нужные реквизиты в текст запроса (через точку прямо в полях выборки или соединением). Если обходится таблица значений со ссылками — выполните один запрос по массиву ссылок с нужными реквизитами и дальше ищите значения в выгруженной таблице по индексу.

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

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

Всегда ли один большой запрос лучше нескольких маленьких?

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