Как оптимизировать вложенные циклы в 1С и ускорить сопоставление таблиц значений?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

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

В чем заключается главная проблема вложенных циклов?

Давайте проанализируем ситуацию, описанную автором темы. У нас есть две таблицы данных: тзРезультат (данные из файла) и Выборка (результат запроса к базе данных). Автор пытается сопоставить их следующим образом:


Для каждого Стр из тзРезультат Цикл
    Для Каждого СтрЗапроса из Выборка Цикл
        Если СокрЛП(СтрЗапроса.НоменклатураТЗПоставщика) = СокрЛП(Стр.Наименование) Тогда
            // Выполнение поиска в базе данных и присвоение ссылки
            Стр.НоменклатураСсылка = Справочники.Номенклатура.НайтиПоНаименованию(СокрЛП(СтрЗапроса.НоменклатураНаша));
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

Давайте посчитаем математическую сложность такого алгоритма. Если в таблице из файла у нас содержится 1 000 строк, а в выборке из базы данных — 5 000 строк, то внутреннее тело цикла выполнится ровно 1 000 * 5 000 = 5 000 000 раз! Такое количество итераций способно намертво подвесить сеанс пользователя на несколько минут. Если же строк будет по 10 000 в каждой таблице, мы получим 100 миллионов итераций, что приведет к перегрузке процессора и перерасходу оперативной памяти.

Кроме того, внутри вложенного цикла выполняется метод Справочники.Номенклатура.НайтиПоНаименованию(). Это критическая ошибка проектирования. Каждый вызов этого метода порождает отдельный, полноценный SQL-запрос к базе данных. Делать тысячи запросов к базе данных в цикле — самый надежный способ «уронить» производительность всей системы.

Рассмотрим, как исправить эту ситуацию с помощью различных подходов.

Решение 1. Быстрое исправление: Использование оператора Прервать

Если вам нужно оставить логику обхода циклом, но минимизировать лишнюю работу, первым делом стоит использовать оператор Прервать. Как только совпадение найдено, нет никакого смысла крутить внутренний цикл дальше до конца.

Давайте посмотрим, как изменится логика с использованием операторов управления циклом:


Для каждого Стр из тзРезультат Цикл
    Если ПустаяСтрока(Стр.Наименование) Тогда
        Продолжить; // Пропускаем пустые строки во внешнем цикле
    КонецЕсли;
    
    Для Каждого СтрЗапроса из Выборка Цикл
        Если СокрЛП(СтрЗапроса.НоменклатураТЗПоставщика) = СокрЛП(Стр.Наименование) Тогда
            // Нашли соответствие, записываем ссылку
            Стр.НоменклатураСсылка = Справочники.Номенклатура.НайтиПоНаименованию(СокрЛП(СтрЗапроса.НоменклатураНаша));
            Прервать; // Немедленно выходим из ВНУТРЕННЕГО цикла
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

Важное примечание: Оператор Прервать прерывает выполнение только того цикла, внутри которого он непосредственно вызван (в данном случае — внутреннего цикла по выборке). Внешний цикл при этом продолжит свою работу и перейдет к следующей строке. Это сократит количество итераций в среднем в два раза, но кардинально проблему производительности не решит. Сложность алгоритма все еще остается близкой к $O(N \times M)$.

Решение 2. Использование индексирования и метода НайтиСтроки

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

По умолчанию поиск по таблице значений происходит линейным перебором, что тоже медленно. Чтобы поиск выполнялся мгновенно, колонку, по которой осуществляется сопоставление, необходимо обязательно проиндексировать. Платформа 1С построит в памяти хэш-таблицу, поиск по которой будет занимать доли миллисекунд.

Давайте разберем этот способ по шагам:

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

Реализуем этот алгоритм на практике:


// Шаг 1. Получаем таблицу значений для поиска
ТаблицаПоиска = Выборка.Выгрузить(); 

// Шаг 2. Обязательно индексируем колонку сопоставления
ТаблицаПоиска.Индексы.Добавить("НоменклатураТЗПоставщика");

// Шаг 3. Запускаем единственный цикл сопоставления
Для каждого Стр из тзРезультат Цикл
    
    ОчищенноеНаименование = СокрЛП(Стр.Наименование);
    Если ПустаяСтрока(ОчищенноеНаименование) Тогда
        Продолжить;
    КонецЕсли;
    
    // Подготавливаем структуру отбора для быстрого поиска по индексу
    ПараметрыОтбора = Новый Структура;
    ПараметрыОтбора.Вставить("НоменклатураТЗПоставщика", ОчищенноеНаименование);
    
    // Поиск по индексу происходит мгновенно (сложность близка к O(1))
    НайденныеСтроки = ТаблицаПоиска.НайтиСтроки(ПараметрыОтбора);
    
    Если НайденныеСтроки.Количество() > 0 Тогда
        // Берем первое найденное совпадение
        СтрокаСовпадения = НайденныеСтроки[0];
        Стр.НоменклатураСсылка = Справочники.Номенклатура.НайтиПоНаименованию(СокрЛП(СтрокаСовпадения.НоменклатураНаша));
    Иначе
        Сообщить("Нет привязки для номенклатуры: " + ОчищенноеНаименование, СтатусСообщения.Важное);
    КонецЕсли;
    
КонецЦикла;

Этот способ работает значительно быстрее вложенных циклов. Время сопоставления сокращается с минут до считанных секунд.

Решение 3. Самый быстрый поиск в памяти — объект «Соответствие»

Объект Соответствие (в других языках программирования известный как Map, Dictionary или ассоциативный массив) — это специальная коллекция «ключ-значение». Поиск значения по ключу в Соответствии работает за константное время $O(1)$ благодаря встроенным алгоритмам хэширования.

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

Посмотрим на пример реализации данного подхода:


// Шаг 1. Создаем и заполняем Соответствие в один проход
КартаСопоставления = Новый Соответствие;
Для каждого СтрЗапроса из Выборка Цикл
    КлючПоиска = СокрЛП(СтрЗапроса.НоменклатураТЗПоставщика);
    Если НЕ ПустаяСтрока(КлючПоиска) Тогда
        // Записываем в соответствие связь: Ключ -> Наше Наименование
        КартаСопоставления.Вставить(КлючПоиска, СтрЗапроса.НоменклатураНаша);
    КонецЕсли;
КонецЦикла;

// Шаг 2. Обходим исходную таблицу и мгновенно получаем значения из соответствия
Для каждого Стр из тзРезультат Цикл
    КлючПоиска = СокрЛП(Стр.Наименование);
    
    // Получение значения по ключу из Соответствия происходит мгновенно
    НайденоНашаНоменклатура = КартаСопоставления.Получить(КлючПоиска);
    
    Если НайденоНашаНоменклатура <> Неопределено Тогда
        // Выполняем поиск ссылки (пока оставим НайтиПоНаименованию для наглядности)
        Стр.НоменклатураСсылка = Справочники.Номенклатура.НайтиПоНаименованию(СокрЛП(НайденоНашаНоменклатура));
    Иначе
        Сообщить("Не удалось сопоставить позицию: " + КлючПоиска, СтатусСообщения.Внимание);
    КонецЕсли;
КонецЦикла;

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

Решение 4. Перенос всей логики в запрос (Best Practice)

Самым надежным, масштабируемым и профессиональным решением является перенос сопоставления на уровень СУБД. В СУБД операции соединения таблиц оптимизированы на самом глубоком уровне. Мы можем передать нашу таблицу значений из файла в качестве параметра запроса, поместить её во временную таблицу, проиндексировать и соединить со справочником номенклатуры с помощью конструкции ЛЕВОЕ СОЕДИНЕНИЕ.

Многие разработчики боятся этого подхода, мотивируя это тем, что в файле данные "грязные" (содержат лишние пробелы в начале или конце), а в запросе нет функции аналогичной СокрЛП(). Однако эту проблему легко решить:

Давайте разберем по шагам, как построить такой оптимальный запрос:


// Шаг 1. Предварительная очистка данных в исходной таблице
// Важно: колонка "Наименование" должна иметь строго определенный строковый тип
Для каждого Стр из тзРезультат Цикл
    Стр.Наименование = СокрЛП(Стр.Наименование);
КонецЦикла;

// Шаг 2. Создаем запрос и передаем таблицу в качестве параметра
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ВнешнийИсточник", тзРезультат);

Запрос.Текст = 
"ВЫБРАТЬ
|	Источник.Наименование КАК НаименованиеПоставщика
|ПОМЕСТИТЬ ВТ_ВнешниеДанные
|ИЗ
|	&ВнешнийИсточник КАК Источник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ.НаименованиеПоставщика КАК НаименованиеПоставщика,
|	ЕСТЬNULL(СпрНоменклатура.Ссылка, ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)) КАК НоменклатураСсылка
|ИЗ
|	ВТ_ВнешниеДанные КАК ВТ
|		ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК СпрНоменклатура
|		ПО ПОДСТРОКА(ВТ.НаименованиеПоставщика, 1, 150) = ПОДСТРОКА(СпрНоменклатура.Наименование, 1, 150)";

// Шаг 3. Выполняем запрос и выгружаем результат сопоставления
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();

// Теперь у нас есть готовая таблица, где для каждого наименования поставщика 
// уже подобрана соответствующая ссылка из базы данных. Без единого вложенного цикла!

Преимущества данного подхода:

Подведем итоги

Мы проанализировали проблему неэффективных вложенных циклов при сопоставлении данных в 1С и изучили четыре различных метода оптимизации алгоритма:

  1. Использование оператора Прервать — подходит для локального и быстрого исправления простого кода, но не решает проблему масштабируемости.
  2. Индексирование таблицы значений и вызов метода НайтиСтроки() — отличный вариант быстрого сопоставления двух таблиц в оперативной памяти без вложенных циклов.
  3. Использование коллекции Соответствие — самый производительный способ поиска по ключу в памяти за счет хэширования ($O(1)$).
  4. Пакетный запрос с временной таблицей и левым соединением — эталонный метод разработки (Best Practice) согласно стандартам платформы «1С:Предприятие». Полностью переносит тяжелые вычисления на сторону СУБД и исключает запросы в цикле.

Применяя эти паттерны проектирования в своей ежедневной работе, вы сможете писать быстрый, красивый и надежный код, который будет легко справляться с любыми объемами обрабатываемых данных.

← На главную