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