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

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

Цикл в цикле в 1С: как избежать вложенных переборов

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

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

Хорошая новость в том, что почти любой вложенный перебор в 1С заменяется одним из стандартных приёмов: соответствием (Map), запросом с соединением таблиц, индексированным поиском по таблице значений или свёрткой. Ниже разобраны рабочие варианты с примерами кода, критерии выбора и способы проверить, что оптимизация действительно сработала.

Как понять, что вложенный цикл — проблема

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

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

Итераций меньше нескольких сотенОптимизация не обязательна

Оставьте вложенный цикл, если он читается проще альтернатив

Десятки тысяч итерацийЗаметная задержка

Замените внутренний поиск соответствием или индексом

Поиск по данным базы в циклеКритично

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

Время растёт быстрее объёма данныхКвадратичная сложность

Ищите вложенный перебор и устраняйте его

Замена поиска соответствием (Соответствие)

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

// Было: цикл в цикле

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

Для Каждого СтрокаОстатка Из Остатки Цикл

Если СтрокаОстатка.Номенклатура = СтрокаЗаказа.Номенклатура Тогда

СтрокаЗаказа.Остаток = СтрокаОстатка.Количество;

КонецЕсли;

КонецЦикла;

КонецЦикла;

// Стало: соответствие

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

Для Каждого СтрокаОстатка Из Остатки Цикл

ОстаткиПоНоменклатуре.Вставить(СтрокаОстатка.Номенклатура, СтрокаОстатка.Количество);

КонецЦикла;

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

Остаток = ОстаткиПоНоменклатуре[СтрокаЗаказа.Номенклатура];

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

СтрокаЗаказа.Остаток = Остаток;

КонецЕсли;

КонецЦикла;

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

Перенос логики в запрос с соединением

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

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

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

"ВЫБРАТЬ

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

| Заказы.Количество КАК Заказано,

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

|ИЗ

| Документ.ЗаказПокупателя.Товары КАК Заказы

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

| ПО Заказы.Номенклатура = Остатки.Номенклатура

|ГДЕ

| Заказы.Ссылка = &Ссылка";

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

bsl
Запрос.УстановитьПараметр("ВнешниеДанные", ТаблицаПрайса);
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// В тексте запроса: ПОМЕСТИТЬ ВТ_Прайс ИЗ &ВнешниеДанные КАК Прайс

Индексы и НайтиСтроки для таблиц значений

Когда данные уже выгружены в таблицу значений и запрос применить нельзя, используйте метод НайтиСтроки вместо полного перебора, предварительно добавив индекс по колонкам поиска. Без индекса НайтиСтроки тоже перебирает всю таблицу, поэтому выигрыш появляется только в связке «индекс + поиск».

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

Отбор = Новый Структура("Номенклатура");

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

Отбор.Номенклатура = СтрокаЗаказа.Номенклатура;

Найденные = ТаблицаОстатков.НайтиСтроки(Отбор);

Для Каждого СтрокаНайденная Из Найденные Цикл

// обработка совпадений

КонецЦикла;

КонецЦикла;

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

Сравнение вариантов

Соответствие

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

Запрос с соединением

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

Индекс + НайтиСтроки

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

Свернуть перед обходом

Много дублей по ключу поиска. Ограничение: теряется детализация строк, подходит не всегда

Групповая обработка вместо поэлементной

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

ВыборкаДокументы = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаДокументы.Следующий() Цикл

ВыборкаСтроки = ВыборкаДокументы.Выбрать();

Пока ВыборкаСтроки.Следующий() Цикл

// каждая строка обработана один раз, поиска нет

КонецЦикла;

КонецЦикла;

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

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

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

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

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

Типичные ошибки
01
Чтение из соответствия без проверки на Неопределено — пропуск данных или исключение
02
Дубли ключей затирают друг друга при построении соответствия — часть строк теряется
03
Создание индекса ради одного поиска — затраты на построение превышают выигрыш
04
Соединение таблиц в запросе без полного условия — взрывной рост результата
05
Замер на тестовых данных вместо боевого объёма — ложный вывод об эффекте

Когда вложенный цикл оставить

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

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

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

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

Обе коллекции загружены в память и поиск идёт по одному полюПостройте соответствие по полю-ключу и уберите внутренний цикл
Данные берутся из информационной базыНапишите один запрос с соединением вместо выгрузки и перебора
Таблица значений и поиск повторяется многократноДобавьте индекс и используйте НайтиСтроки со структурой отбора
Коллекции малы и ограничены логикойОставьте вложенный цикл — оптимизация не окупится
Внутренний цикл обрабатывает подчинённые строки без поискаСложность линейная, переписывать не нужно
Итог

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

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

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

Чем соответствие быстрее вложенного цикла?

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

Что делать, если по одному ключу несколько строк?

Храните в значении соответствия массив: при построении проверяйте наличие ключа, создавайте массив при первом обращении и добавляйте в него строки. Альтернатива — индексированная таблица значений с НайтиСтроки, которая изначально возвращает все совпадения.

Можно ли соединить в запросе таблицу значений с таблицей базы?

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

Почему после добавления индекса код не ускорился?

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

Как измерить время выполнения фрагмента кода?

Сохраните значение ТекущаяУниверсальнаяДатаВМиллисекундах до фрагмента и вычтите его из значения после — получите длительность в миллисекундах. Для детального анализа используйте замер производительности в конфигураторе, который покажет время по каждой строке модуля.

Медленный запрос — это тоже проблема вложенных циклов?

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