Цикл во встроенном языке 1С: от синтаксиса до быстрого кода
Содержание
Во встроенном языке 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 дополнительных обращений к базе. Платформа кеширует прочитанные объекты, но кеш живёт недолго и не спасает при разных контрагентах. - 🔁 Вызов сервера с клиента. Если цикл идёт на клиенте и на каждой итерации вызывает процедуру
&НаСервере, каждая итерация — отдельный сетевой вызов с передачей контекста формы.
Точечное чтение одного-двух реквизитов вне цикла делайте через функции БСП ОбщегоНазначения.ЗначениеРеквизитаОбъекта и ОбщегоНазначения.ЗначенияРеквизитовОбъекта. Они читают только запрошенные поля одним запросом.
Отдельная история — ПолучитьОбъект() внутри цикла. Объект нужен только если вы его меняете и записываете. Для чтения хватает полей выборки или запроса.
Выборка из результата запроса вместо обхода объектов
Тот же расчёт через запрос получает только нужные поля. Обращение Реализация.Контрагент.ИНН внутри текста запроса платформа превращает в соединение таблиц на стороне СУБД, а не в отдельное чтение объекта.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК Документ,
| Реализация.Контрагент.ИНН КАК ИНН,
| Реализация.СуммаДокумента КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &Начало И &Конец
| И Реализация.Проведен";
Запрос.УстановитьПараметр("Начало", НачалоПериода);
Запрос.УстановитьПараметр("Конец", КонецПериода);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Выборка.Документ, Выборка.ИНН, Выборка.Сумма
КонецЦикла;
Если нужна только сумма, цикл не нужен вовсе: посчитайте СУММА(Реализация.СуммаДокумента) в запросе и получите одно число.
Выбрать() или ВыгрузитьВТаблицу()
У результата запроса два способа получить данные:
Выбрать()— последовательный обход вперёд. Не создаёт дополнительную копию данных в виде таблицы. Лучший вариант для одного прохода.Выгрузить()— таблица значений в памяти. Нужна, когда по данным ищут, сортируют, группируют или проходят несколько раз.
Выгрузка ради одного Для каждого — лишняя работа и лишняя память. Выборка ради поиска по ней — неудобно: по выборке нельзя вернуться назад.
Для иерархических данных и итогов есть Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам): внешняя выборка идёт по группировкам, вложенная — по детальным записям. Это заменяет ручную группировку в цикле.
Цикл по объектам против выборки запроса: что выбрать
| Подход | Что читает из базы | Когда оправдан | Риски |
|---|---|---|---|
Выбрать() у справочника или документа | все реквизиты шапки | простой обход, когда объектов немного | точка от ссылки внутри — лишние чтения |
ПолучитьОбъект() в цикле | объект целиком с табличными частями | нужно изменить и записать | медленно, блокировки при записи |
Запрос + Выбрать() | только поля из текста запроса | чтение, отчёты, расчёты | нет |
Запрос + Выгрузить() | только нужные поля, всё в память | поиск, сортировка, несколько проходов | память на больших объёмах |
| Расчёт целиком в запросе | итоговые числа | суммы, остатки, количества | сложнее отладка текста запроса |
Правило простое: читать — запросом, изменять — объектом. Для изменения объектов всё равно сначала отберите ссылки запросом с условиями, а не перебирайте весь справочник с проверкой Если внутри.
Вложенные циклы и запросы в цикле: проблема N+1
Проблема N+1 — когда на каждую из N строк выполняется ещё один запрос. Типичный случай — цена для каждой строки табличной части:
// Так делать не нужно: 1 запрос на каждую строку
Для Каждого Строка Из Объект.Товары Цикл
Запрос.УстановитьПараметр("Номенклатура", Строка.Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
// ...
КонецЦикла;
На документе в 300 строк это 300 обращений к базе вместо одного. Переделка в два шага: один запрос по списку, затем поиск в соответствии.
Список = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
Запрос = Новый Запрос;
// Имя регистра и измерений зависит от конфигурации
Запрос.Текст =
"ВЫБРАТЬ
| Цены.Номенклатура КАК Номенклатура,
| Цены.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, Номенклатура В (&Список)) КАК Цены";
Запрос.УстановитьПараметр("Дата", Объект.Дата);
Запрос.УстановитьПараметр("Список", Список);
Цены = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Цены.Вставить(Выборка.Номенклатура, Выборка.Цена);
КонецЦикла;
Для Каждого Строка Из Объект.Товары Цикл
Цена = Цены.Получить(Строка.Номенклатура);
Если Цена = Неопределено Тогда
Продолжить;
КонецЕсли;
Строка.Цена = Цена;
КонецЦикла;
Получить() у соответствия ищет по ключу без перебора и возвращает Неопределено, если ключа нет.
Вложенный цикл по двум таблицам
Сверка двух таблиц «каждая строка с каждой» на 5 000 и 5 000 строк — это 25 миллионов сравнений. Три способа сократить:
- Сложить вторую таблицу в
Соответствиепо ключу и искать за одно обращение. - Добавить индекс в таблицу значений:
Таблица.Индексы.Добавить("Номенклатура"), затем искать черезНайтиСтроки(Новый Структура("Номенклатура", Значение)). - Передать обе таблицы в запрос как параметры, поместить во временные таблицы и соединить средствами СУБД.
Третий вариант лучший, когда данные и так пришли из базы: соединение в запросе дешевле любого цикла во встроенном языке.
Долгие циклы: фоновые задания, транзакции и прерывание
Если цикл обрабатывает десятки тысяч объектов и всё же нужен, разделите две задачи: не держать пользователя и не держать блокировки.
Вынести цикл в фоновое задание
Форма не должна висеть, пока идёт пересчёт. Платформенный способ — ФоновыеЗадания.Выполнить. Метод запускает экспортную процедуру серверного общего модуля:
Параметры = Новый Массив;
Параметры.Добавить(НачалоПериода);
Параметры.Добавить(КонецПериода);
Задание = ФоновыеЗадания.Выполнить("ОбработкаДанныхСервер.ПересчитатьЦены", Параметры, , "Пересчёт цен");
В конфигурациях на БСП удобнее ДлительныеОперации.ВыполнитьВФоне: модуль сам отслеживает завершение и показывает пользователю ход выполнения.
Асинхронность клиента (Асинх и Ждать, платформа 8.3.18 и новее) не ускоряет обработку данных. Она нужна для диалогов и ожидания ответа, а тяжёлый цикл по базе по-прежнему должен идти на сервере.
Записывать порциями, а не одной транзакцией
Запись 50 000 объектов в одной транзакции держит блокировки до самого конца и при ошибке на последнем объекте откатывает всё. Запись без явной транзакции — каждый объект в своей — медленнее и оставляет базу в промежуточном состоянии. Компромисс — порции:
Счетчик = 0;
НачатьТранзакцию();
Попытка
Пока Выборка.Следующий() Цикл
Объект = Выборка.Ссылка.ПолучитьОбъект();
Объект.Комментарий = "Обработано";
Объект.Записать();
Счетчик = Счетчик + 1;
Если Счетчик % 500 = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
При ошибке откатится только текущая порция, а уже зафиксированные 500, 1000, 1500 объектов останутся записанными. Учитывайте это: перезапуск обработки должен уметь пропускать готовые объекты.
Массовую запись в рабочей базе запускайте после резервной копии и вне рабочего времени: записываемые объекты блокируются, пользователи с ними будут ждать.
Флаг Объект.ОбменДанными.Загрузка = Истина ускоряет запись, потому что отключает обработчики ПередЗаписью, ПриЗаписи и проверки. Ставьте его только когда точно знаете, что эти обработчики не нужны: иначе в базу попадут данные без контроля и без зависимых записей.
Как найти медленный цикл в своём коде
Не угадывайте, где тормозит, — меряйте.
- В конфигураторе запустите отладку и включите
Отладка → Замер производительности. - Выполните медленную операцию в режиме Предприятия.
- Остановите замер. Строки кода с наибольшей долей времени и числом выполнений покажут, какое тело цикла дорогое.
Для быстрой прикидки без отладчика оберните цикл замером времени:
Старт = ТекущаяУниверсальнаяДатаВМиллисекундах();
// цикл
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Старт;
Сообщить(СтрШаблон("Цикл выполнен за %1 мс", Формат(Длительность, "ЧГ=0")));
Если строка Запрос.Выполнить() или ПолучитьОбъект() в замере выполнилась столько же раз, сколько итераций у цикла, — вы нашли N+1.
Частые вопросы
Какой цикл в 1С самый быстрый?
Для, Для каждого и Пока на одной и той же коллекции почти незаметна. Скорость определяет тело цикла: обращения к базе, вызовы сервера и запись объектов.Можно ли выйти сразу из двух вложенных циклов?
Прервать выходит только из текущего цикла. Для выхода из внешнего заведите флаг, проверяйте его после внутреннего цикла и вызывайте Прервать ещё раз. Или вынесите вложенные циклы в отдельную функцию и выходите через Возврат.Почему цикл на клиенте работает медленнее, чем тот же код на сервере?
&НаСервере или &НаСервереБезКонтекста и вызывайте её один раз.Сколько строк можно передать в запрос параметром для условия В (&Список)?
ПОМЕСТИТЬ и соединиться с ней.Нужен ли Попытка … Исключение внутри цикла?
ЗаписьЖурналаРегистрации и переходите к следующему элементу через Продолжить.