Цикл во встроенном языке 1С: от синтаксиса до быстрого кода

· 10 мин

Содержание

Во встроенном языке 1С три вида циклов: Для … По со счётчиком, Для каждого … Из для обхода коллекций и Пока с условием. Сам цикл почти никогда не тормозит. Медленно работает то, что стоит внутри: запрос на каждой итерации, обращение к реквизиту через точку от ссылки, вызов сервера с клиента, запись объекта.

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

Три вида циклов в 1С и как каждый проходит

Все три конструкции закрываются одним словом КонецЦикла;. Внутри работают два оператора управления:

  • ⏭️ Продолжить — пропустить остаток тела и перейти к следующей итерации;
  • ⛔ Прервать — выйти из цикла сразу, код продолжится после КонецЦикла.

Цикл со счётчиком «Для … По»

Счётчик растёт на 1 от начального значения до конечного включительно. Шаг задать нельзя: нужен шаг 2 или обратный ход — считайте индекс сами.

Для Сч = 1 По 10 Цикл
    Сумма = Сумма + Сч;
КонецЦикла;

После Для Сч = 1 По 10 тело выполнится 10 раз. Если начальное значение больше конечного, тело не выполнится ни разу.

Обход коллекции «Для каждого … Из»

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

Для Каждого СтрокаТовара Из Объект.Товары Цикл
    СтрокаТовара.Сумма = СтрокаТовара.Количество * СтрокаТовара.Цена;
КонецЦикла;

Для соответствия и структуры элемент — пара с полями Ключ и Значение.

Цикл с условием «Пока»

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

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

Бесконечный цикл на клиенте можно прервать сочетанием Ctrl+Break, если внутри вызывается ОбработкаПрерыванияПользователя(). Добавляйте этот вызов в длинные клиентские циклы.

Как «Для каждого» обходит коллекцию и почему нельзя удалять на ходу

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

Пример: в табличной части 5 строк, у второй и третьей количество 0. Цикл удалит вторую строку, третья станет второй, а итератор уже перейдёт к новой третьей. Строка с нулём останется.

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

Всего = Объект.Товары.Количество();
Для Сч = 1 По Всего Цикл
    Индекс = Всего - Сч;
    Строка = Объект.Товары[Индекс];
    Если Строка.Количество = 0 Тогда
        Объект.Товары.Удалить(Строка);
    КонецЕсли;
КонецЦикла;

При 5 строках индексы пойдут 4, 3, 2, 1, 0. Удаление строки с индексом 2 не трогает строки 0 и 1, которые ещё впереди.

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

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

Почему обход объектов в цикле работает медленно

Классический «медленный» цикл выглядит безобидно:

Выборка = Документы.РеализацияТоваровУслуг.Выбрать(НачалоПериода, КонецПериода);
Пока Выборка.Следующий() Цикл
    ИНН = Выборка.Контрагент.ИНН;
    Сумма = Сумма + Выборка.СуммаДокумента;
КонецЦикла;

Здесь три источника лишней работы.

  • 📦 Выборка объектов читает всё. Документы.<Имя>.Выбрать() достаёт из базы все реквизиты шапки, даже если нужен один.
  • 🔗 Обращение через точку от ссылки. Выборка.Контрагент.ИНН заставляет платформу прочитать объект контрагента целиком, вместе с табличными частями. На 10 000 документов это до 10 000 дополнительных обращений к базе. Платформа кеширует прочитанные объекты, но кеш живёт недолго и не спасает при разных контрагентах.
  • 🔁 Вызов сервера с клиента. Если цикл идёт на клиенте и на каждой итерации вызывает процедуру &НаСервере, каждая итерация — отдельный сетевой вызов с передачей контекста формы.

Точечное чтение одного-двух реквизитов вне цикла делайте через функции БСП ОбщегоНазначения.ЗначениеРеквизитаОбъекта и ОбщегоНазначения.ЗначенияРеквизитовОбъекта. Они читают только запрошенные поля одним запросом.

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

Выборка из результата запроса вместо обхода объектов

Тот же расчёт через запрос получает только нужные поля. Обращение Реализация.Контрагент.ИНН внутри текста запроса платформа превращает в соединение таблиц на стороне СУБД, а не в отдельное чтение объекта.

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

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Выборка.Документ, Выборка.ИНН, Выборка.Сумма
КонецЦикла;

Если нужна только сумма, цикл не нужен вовсе: посчитайте СУММА(Реализация.СуммаДокумента) в запросе и получите одно число.

Выбрать() или ВыгрузитьВТаблицу()

У результата запроса два способа получить данные:

  1. Выбрать() — последовательный обход вперёд. Не создаёт дополнительную копию данных в виде таблицы. Лучший вариант для одного прохода.
  2. Выгрузить() — таблица значений в памяти. Нужна, когда по данным ищут, сортируют, группируют или проходят несколько раз.

Выгрузка ради одного Для каждого — лишняя работа и лишняя память. Выборка ради поиска по ней — неудобно: по выборке нельзя вернуться назад.

Для иерархических данных и итогов есть Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам): внешняя выборка идёт по группировкам, вложенная — по детальным записям. Это заменяет ручную группировку в цикле.

Цикл по объектам против выборки запроса: что выбрать

ПодходЧто читает из базыКогда оправданРиски
Выбрать() у справочника или документавсе реквизиты шапкипростой обход, когда объектов немноготочка от ссылки внутри — лишние чтения
ПолучитьОбъект() в циклеобъект целиком с табличными частяминужно изменить и записатьмедленно, блокировки при записи
Запрос + Выбрать()только поля из текста запросачтение, отчёты, расчётынет
Запрос + Выгрузить()только нужные поля, всё в памятьпоиск, сортировка, несколько проходовпамять на больших объёмах
Расчёт целиком в запросеитоговые числасуммы, остатки, количествасложнее отладка текста запроса

Правило простое: читать — запросом, изменять — объектом. Для изменения объектов всё равно сначала отберите ссылки запросом с условиями, а не перебирайте весь справочник с проверкой Если внутри.

Вложенные циклы и запросы в цикле: проблема N+1

Проблема N+1 — когда на каждую из N строк выполняется ещё один запрос. Типичный случай — цена для каждой строки табличной части:

// Так делать не нужно: 1 запрос на каждую строку
Для Каждого Строка Из Объект.Товары Цикл
    Запрос.УстановитьПараметр("Номенклатура", Строка.Номенклатура);
    Выборка = Запрос.Выполнить().Выбрать();
    // ...
КонецЦикла;

На документе в 300 строк это 300 обращений к базе вместо одного. Переделка в два шага: один запрос по списку, затем поиск в соответствии.

Список = Объект.Товары.ВыгрузитьКолонку("Номенклатура");

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

Цены = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    Цены.Вставить(Выборка.Номенклатура, Выборка.Цена);
КонецЦикла;

Для Каждого Строка Из Объект.Товары Цикл
    Цена = Цены.Получить(Строка.Номенклатура);
    Если Цена = Неопределено Тогда
        Продолжить;
    КонецЕсли;
    Строка.Цена = Цена;
КонецЦикла;

Получить() у соответствия ищет по ключу без перебора и возвращает Неопределено, если ключа нет.

Вложенный цикл по двум таблицам

Сверка двух таблиц «каждая строка с каждой» на 5 000 и 5 000 строк — это 25 миллионов сравнений. Три способа сократить:

  1. Сложить вторую таблицу в Соответствие по ключу и искать за одно обращение.
  2. Добавить индекс в таблицу значений: Таблица.Индексы.Добавить("Номенклатура"), затем искать через НайтиСтроки(Новый Структура("Номенклатура", Значение)).
  3. Передать обе таблицы в запрос как параметры, поместить во временные таблицы и соединить средствами СУБД.

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

Долгие циклы: фоновые задания, транзакции и прерывание

Если цикл обрабатывает десятки тысяч объектов и всё же нужен, разделите две задачи: не держать пользователя и не держать блокировки.

Вынести цикл в фоновое задание

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

Параметры = Новый Массив;
Параметры.Добавить(НачалоПериода);
Параметры.Добавить(КонецПериода);
Задание = ФоновыеЗадания.Выполнить("ОбработкаДанныхСервер.ПересчитатьЦены", Параметры, , "Пересчёт цен");

В конфигурациях на БСП удобнее ДлительныеОперации.ВыполнитьВФоне: модуль сам отслеживает завершение и показывает пользователю ход выполнения.

Асинхронность клиента (Асинх и Ждать, платформа 8.3.18 и новее) не ускоряет обработку данных. Она нужна для диалогов и ожидания ответа, а тяжёлый цикл по базе по-прежнему должен идти на сервере.

Записывать порциями, а не одной транзакцией

Запись 50 000 объектов в одной транзакции держит блокировки до самого конца и при ошибке на последнем объекте откатывает всё. Запись без явной транзакции — каждый объект в своей — медленнее и оставляет базу в промежуточном состоянии. Компромисс — порции:

Счетчик = 0;
НачатьТранзакцию();
Попытка
    Пока Выборка.Следующий() Цикл
        Объект = Выборка.Ссылка.ПолучитьОбъект();
        Объект.Комментарий = "Обработано";
        Объект.Записать();
        Счетчик = Счетчик + 1;
        Если Счетчик % 500 = 0 Тогда
            ЗафиксироватьТранзакцию();
            НачатьТранзакцию();
        КонецЕсли;
    КонецЦикла;
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки;

При ошибке откатится только текущая порция, а уже зафиксированные 500, 1000, 1500 объектов останутся записанными. Учитывайте это: перезапуск обработки должен уметь пропускать готовые объекты.

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

Флаг Объект.ОбменДанными.Загрузка = Истина ускоряет запись, потому что отключает обработчики ПередЗаписью, ПриЗаписи и проверки. Ставьте его только когда точно знаете, что эти обработчики не нужны: иначе в базу попадут данные без контроля и без зависимых записей.

Как найти медленный цикл в своём коде

Не угадывайте, где тормозит, — меряйте.

  1. В конфигураторе запустите отладку и включите Отладка → Замер производительности.
  2. Выполните медленную операцию в режиме Предприятия.
  3. Остановите замер. Строки кода с наибольшей долей времени и числом выполнений покажут, какое тело цикла дорогое.

Для быстрой прикидки без отладчика оберните цикл замером времени:

Старт = ТекущаяУниверсальнаяДатаВМиллисекундах();
// цикл
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Старт;
Сообщить(СтрШаблон("Цикл выполнен за %1 мс", Формат(Длительность, "ЧГ=0")));

Если строка Запрос.Выполнить() или ПолучитьОбъект() в замере выполнилась столько же раз, сколько итераций у цикла, — вы нашли N+1.

Проверка цикла перед выкладкой
Выполнено: 0 / 7

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

Какой цикл в 1С самый быстрый?
Разница между Для, Для каждого и Пока на одной и той же коллекции почти незаметна. Скорость определяет тело цикла: обращения к базе, вызовы сервера и запись объектов.
Можно ли выйти сразу из двух вложенных циклов?
Прервать выходит только из текущего цикла. Для выхода из внешнего заведите флаг, проверяйте его после внутреннего цикла и вызывайте Прервать ещё раз. Или вынесите вложенные циклы в отдельную функцию и выходите через Возврат.
Почему цикл на клиенте работает медленнее, чем тот же код на сервере?
На клиенте обычно нет прямого доступа к базе, и каждое чтение данных превращается в вызов сервера. Перенесите весь цикл в процедуру &НаСервере или &НаСервереБезКонтекста и вызывайте её один раз.
Сколько строк можно передать в запрос параметром для условия В (&Список)?
Жёсткого предела во встроенном языке нет, но очень длинный список раздувает текст запроса к СУБД. При десятках тысяч значений надёжнее поместить их во временную таблицу через ПОМЕСТИТЬ и соединиться с ней.
Нужен ли Попытка … Исключение внутри цикла?
Только если ошибка на одном элементе не должна останавливать обработку остальных. Тогда записывайте ошибку в журнал регистрации через ЗаписьЖурналаРегистрации и переходите к следующему элементу через Продолжить.