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

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

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

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

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


Функция СравнениеТаблицЗначений_Запрос(Таблица1, Таблица2)
    Запрос = Новый Запрос;
    Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
    
    // Передача данных во временные таблицы — самая долгая часть
    Запрос.Текст = "ВЫБРАТЬ * ПОМЕСТИТЬ Т1 ИЗ &Таблица1 КАК Т1;
                   |ВЫБРАТЬ * ПОМЕСТИТЬ Т2 ИЗ &Таблица2 КАК Т2;";
    Запрос.УстановитьПараметр("Таблица1", Таблица1);
    Запрос.УстановитьПараметр("Таблица2", Таблица2);
    Запрос.Выполнить();
    
    // Само сравнение (например, FULL JOIN) отработает быстро на стороне SQL, 
    // но общие затраты времени могут быть выше, чем у методов в памяти.
КонецФункции

Анализ показывает, что при объемах до 50 000 – 100 000 строк методы, работающие непосредственно в оперативной памяти (в коллекции), зачастую обходят запросы по скорости именно из-за отсутствия накладных расходов на сериализацию данных для SQL-сервера.

Метод 1: Сравнение через сериализацию (ЗначениеВСтрокуВнутр)

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


Функция ТаблицыИдентичны(Таблица1, Таблица2, КолонкиСортировки)
    Если Таблица1.Количество() <> Таблица2.Количество() Тогда
        Возврат Ложь;
    КонецЕсли;
    
    Таблица1.Сортировать(КолонкиСортировки);
    Таблица2.Сортировать(КолонкиСортировки);
    
    Возврат ЗначениеВСтрокуВнутр(Таблица1) = ЗначениеВСтрокуВнутр(Таблица2);
КонецФункции

Важный момент: этот способ крайне эффективен по написанию (всего одна строка), но он потребляет много памяти на больших объемах и не позволяет выделить конкретные изменения.

Метод 2: Алгоритм «Двойной свертки» (Универсальный метод)

Разберем один из самых элегантных методов поиска различий. Идея заключается в объединении двух таблиц, назначении им признаков «Знак» (1 и -1) и последующей группировке. Проанализируем реализацию этого алгоритма:

  1. Добавляем в обе таблицы служебную колонку Знак.
  2. В первой таблице заполняем её значением 1, во второй — -1.
  3. Объединяем таблицы в одну общую ТЗ.
  4. Сворачиваем общую таблицу по всем сравниваемым полям, суммируя колонку Знак.

Результат будет следующим: если после свертки Знак = 0, то строки в обеих таблицах были идентичны. Если Знак = 1 — строка есть только в первой таблице. Если Знак = -1 — только во второй.


Функция СравнениеМетодомСвертки(Т1, Т2, ИменаКолонок)
    ОбщаяТЗ = Т1.Скопировать();
    ОбщаяТЗ.Колонки.Добавить("Знак");
    ОбщаяТЗ.ЗаполнитьЗначения(1, "Знак");
    
    Для Каждого СтрТ2 Из Т2 Цикл
        НоваяСтр = ОбщаяТЗ.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтр, СтрТ2);
        НоваяСтр.Знак = -1;
    КонецЦикла;
    
    ОбщаяТЗ.Свернуть(ИменаКолонок, "Знак");
    
    // Удаляем идентичные строки (где Знак = 0)
    МертвыеСтроки = ОбщаяТЗ.НайтиСтроки(Новый Структура("Знак", 0));
    Для Каждого Стр Из МертвыеСтроки Цикл
        ОбщаяТЗ.Удалить(Стр);
    КонецЦикла;
    
    Возврат ОбщаяТЗ;
КонецФункции

Метод 3: Использование «Соответствия» (Hash Map)

Рассмотрим ситуацию, когда одна таблица является «эталоном», а вторую нужно проверить на изменения. Использование объекта Соответствие позволяет реализовать поиск со скоростью O(1), что критически важно для больших массивов данных.


Функция СравнитьЧерезСоответствие(Т1, Т2)
    Ответ = Новый Соответствие;
    ХэшКарта = Новый Соответствие;
    
    // Индексируем вторую таблицу в памяти
    Для Каждого Строка Из Т2 Цикл
        ХэшКарта.Вставить(Строка[0], Строка); // Предполагаем, что колонка 0 - ключ
    КонецЦикла;
    
    Для Каждого Ведущая Из Т1 Цикл
        Ключ = Ведущая[0];
        Ведомая = ХэшКарта[Ключ];
        
        Если Ведомая = Неопределено Тогда
            Ответ.Вставить(Ключ, "Удалена");
        Иначе
            // Построчное сравнение колонок
            Для ё = 1 По Т1.Колонки.Количество() - 1 Цикл
                Если Ведущая[ё] <> Ведомая[ё] Тогда
                    Ответ.Вставить(Ключ, "Изменена");
                    Прервать;
                КонецЕсли;
            КонецЦикла;
            ХэшКарта.Удалить(Ключ);
        КонецЕсли;
    КонецЦикла;
    
    // Остатки в ХэшКарте — это добавленные строки
    Для Каждого Элемент Из ХэшКарта Цикл
        Ответ.Вставить(Элемент.Ключ, "Добавлена");
    КонецЦикла;
    
    Возврат Ответ;
КонецФункции

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

Метод 4: Алгоритм слияния (Merge Join) для отсортированных таблиц

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

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

Посмотрим на оптимизированный код цикла:


Индекс1 = 0; Индекс2 = 0;
ВГраница1 = Т1.Количество() - 1;
ВГраница2 = Т2.Количество() - 1;

Пока Индекс1 <= ВГраница1 И Индекс2 <= ВГраница2 Цикл
    Стр1 = Т1[Индекс1];
    Стр2 = Т2[Индекс2];
    Ключ1 = Стр1[0];
    Ключ2 = Стр2[0];
    
    Если Ключ1 < Ключ2 Тогда
        // Обработка удаления
        Индекс1 = Индекс1 + 1;
    ИначеЕсли Ключ1 > Ключ2 Тогда
        // Обработка добавления
        Индекс2 = Индекс2 + 1;
    Иначе
        // Ключи равны, сравниваем данные
        Для ё = 1 По ВГраницаКолонок Цикл
            Если Стр1[ё] <> Стр2[ё] Тогда
                // Строка изменена
                Прервать;
            КонецЕсли;
        КонецЦикла;
        Индекс1 = Индекс1 + 1;
        Индекс2 = Индекс2 + 1;
    КонецЕсли;
КонецЦикла;

Практические советы по оптимизации

Рассмотрим несколько хитростей, которые помогут ускорить работу кода в разы:

  1. Кэширование обращений: Обращение к колонке по имени Строка["ИмяКолонки"] работает медленнее, чем по индексу Строка[0]. Если вы планируете большой цикл, заранее получите индексы колонок.
  2. Использование объекта «СравнениеЗначений»: При сравнении сложных типов (например, СправочникСсылка и Строка) стандартные операторы могут вызвать ошибку. Объект СравнениеЗначений работает стабильнее и быстрее в универсальных алгоритмах.
  3. Хеширование длинных строк: Если в таблице много текстовых полей, сравнение которых занимает время, можно предварительно рассчитать хэш всей строки (например, через JSON или MD5) и сравнивать только его. В платформе 8.3.11+ для этого есть объект ХешированиеДанных.
  4. Объект «Индексы»: Если вы часто используете НайтиСтроки(), обязательно добавьте индекс: Таблица.Индексы.Добавить("КолонкаКлюч"). Это превращает линейный поиск в логарифмический.

Проанализировав ситуацию, можно сделать вывод: для разового сравнения небольших таблиц (до 1000 строк) лучше использовать Метод 2 (Свертка) из-за простоты реализации. Для высоконагруженных участков кода и больших объемов данных следует выбирать Метод 3 (Соответствие) или Метод 4 (Слияние), так как они обеспечивают наилучшую производительность.

← На главную