Как оптимизировать поиск и сопоставление состояний в коллекциях значений 1С?

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

При расчете себестоимости и распределении затрат в конфигурациях на платформе 1С (в частности, в алгоритмах расчета себестоимости УПП) часто возникает задача идентификации уникальных аналитических комбинаций — состояний источников и приемников. Нередко базовый код решает эту задачу тройным вложенным циклом, что приводит к критическому падению производительности на больших объемах данных. Разберем, почему исходный алгоритм работает медленно, и рассмотрим эффективные варианты его оптимизации.

В чем заключается проблема исходного алгоритма

Проанализируем исходный код сопоставления. Для каждой строки таблицы Таб выполняется поиск в коллекции СоотвПараметровСостояний:


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

Выясним причину низкой скорости работы данной конструкции:

  1. Квадратичная вычислительная сложность: сложность алгоритма составляет O(N × M × K), где N — число строк таблицы, M — количество накопленных уникальных состояний, а K — количество аналитических колонок. С ростом количества уникальных комбинаций цикл замедляется в геометрической прогрессии.
  2. Неэффективное использование объекта Соответствие: объект коллекции перебирается полным циклом Для Каждого, вместо прямого извлечения элемента по ключу за время O(1).
  3. Накладные расходы динамического доступа: обращение вида Строка[Элемент.Ключ] заставляет платформу на каждой итерации искать идентификатор свойства по строковому имени в хэш-таблице метаданных объекта.
  4. Лишние условные операторы: проверка условий пропуска колонок (ВременнаяРазница, ПостояннаяРазница) выполняется многократно внутри самого глубокого вложенного цикла.

Способ 1. Использование составного ключа в объекте Соответствие (Хэш-поиск)

Посмотрим на объект Структура. Он является ссылочным типом данных, поэтому сравнение двух структур с одинаковыми свойствами (Новый Структура("А", 1) = Новый Структура("А", 1)) вернет Ложь. По этой причине структуру нельзя напрямую сделать ключом в Соответствии.

Однако мы можем сформировать составной строковый ключ или сериализовать значения ключевых колонок, чтобы использовать мгновенный поиск по ключу за время O(1). Предварительно исключим неиспользуемые колонки из перебора.


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

// Вспомогательное соответствие: Ключ - уникальная строка параметров, Значение - Индекс
КартаИндексов = Новый Соответствие;
Разделитель = Символы.ВК;

Для Каждого Строка Из Таб Цикл
    // --- Обработка источника ---
    КлючИсточника = "";
    Для Каждого ИмяКолонки Из МассивКолонок Цикл
        КлючИсточника = КлючИсточника + Разделитель + Строка(ЗначениеВСтрокуВнутр(Строка[ИмяКолонки]));
    КонецЦикла;
    
    ИндексИсточника = КартаИндексов[КлючИсточника];
    Если ИндексИсточника = Неопределено Тогда
        ИндексИсточника = СоотвПараметровСостояний.Количество();
        
        СтрСост = Новый Структура;
        Для Каждого ИмяКолонки Из МассивКолонок Цикл
            СтрСост.Вставить(ИмяКолонки, Строка[ИмяКолонки]);
        КонецЦикла;
        
        СоотвПараметровСостояний.Вставить(ИндексИсточника, СтрСост);
        КартаИндексов.Вставить(КлючИсточника, ИндексИсточника);
    КонецЕсли;
    Строка.Источник = ИндексИсточника;
    
    // --- Обработка приемника ---
    КлючПриемника = "";
    Для Каждого ИмяКолонки Из МассивКолонок Цикл
        КлючПриемника = КлючПриемника + Разделитель + Строка(ЗначениеВСтрокуВнутр(Строка[ИмяКолонки + ПрефиксПараметровНовогоСостояния]));
    КонецЦикла;
    
    ИндексПриемника = КартаИндексов[КлючПриемника];
    Если ИндексПриемника = Неопределено Тогда
        ИндексПриемника = СоотвПараметровСостояний.Количество();
        
        СтрСост = Новый Структура;
        Для Каждого ИмяКолонки Из МассивКолонок Цикл
            СтрСост.Вставить(ИмяКолонки, Строка[ИмяКолонки + ПрефиксПараметровНовогоСостояния]);
        КонецЦикла;
        
        СоотвПараметровСостояний.Вставить(ИндексПриемника, СтрСост);
        КартаИндексов.Вставить(КлючПриемника, ИндексПриемника);
    КонецЕсли;
    Строка.Приемник = ИндексПриемника;
КонецЦикла;

Такой подход сокращает сложность с O(N × M × K) до линейной O(N × K), обеспечивая многократный прирост производительности.

Способ 2. Пакетная агрегация и соединение через Запрос

Рассмотрим альтернативное решение, наиболее естественное для архитектуры 1С: перенос всей логики в СУБД. Реляционная база данных выполняет группировки и соединения несравнимо быстрее интерпретатора платформы за счет алгоритмов Hash Match и Merge Join.

Разберем по шагам логику решения через запрос:

  1. Помещаем таблицу Таб во временную таблицу в менеджере временных таблиц.
  2. Выбираем через ОБЪЕДИНИТЬ все уникальные комбинации полей источника и полей приемника.
  3. С помощью ключевого слова АВТОНОМЕРЗАПИСИ() формируем суррогатный числовой идентификатор для каждого состояния.
  4. Левым соединением сопоставляем исходную таблицу с полученной таблицей состояний — отдельно по полям источника, отдельно по полям приемника.

Этот способ освобождает память виртуальной машины 1С и исключает ручной перебор больших коллекций.

Способ 3. Реализация алгоритма слияния (Merge Join) в памяти

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

Посмотрим на пример сопоставления двух таблиц по общему ключу поиска:


Таблица1.Сортировать("КлючПоиска Возр");
Таблица2.Сортировать("КлючПоиска Возр");

Счетчик1 = 0;
Счетчик2 = 0;
Всего1 = Таблица1.Количество();
Всего2 = Таблица2.Количество();

Пока Счетчик1 < Всего1 И Счетчик2 < Всего2 Цикл
    СтрокаТаблицы1 = Таблица1[Счетчик1];
    СтрокаТаблицы2 = Таблица2[Счетчик2];
    
    Если СтрокаТаблицы1.КлючПоиска > СтрокаТаблицы2.КлючПоиска Тогда
        Счетчик2 = Счетчик2 + 1;
    ИначеЕсли СтрокаТаблицы1.КлючПоиска < СтрокаТаблицы2.КлючПоиска Тогда
        Счетчик1 = Счетчик1 + 1;
    Иначе
        // Ключи совпали - переносим необходимые реквизиты
        СтрокаТаблицы1.ЗначениеИзТаблицы2 = СтрокаТаблицы2.Значение;
        Счетчик1 = Счетчик1 + 1;
    КонецЕсли;
КонецЦикла;

Сложность алгоритма слияния составляет O(N log N) на этапе сортировки и O(N + M) при непосредственном обходе указателями, что на порядки быстрее вложенных циклов.

Сводные рекомендации по оптимизации

  1. Никогда не выполняйте линейный перебор коллекции для поиска элемента: используйте методы НайтиСтроки() по индексу, прямое получение по ключу из Соответствия или алгоритм слияния.
  2. Выносите константные операции за пределы циклов: отфильтруйте имена колонок один раз до начала обхода строк, чтобы не выполнять ветвления Если ... Тогда миллионы раз.
  3. Не создавайте индексы динамически в теле цикла: добавление строк в ТаблицуЗначений с настроенными индексами приводит к постоянной их перестройке, что существенно снижает скорость наполнения таблицы.
← На главную