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

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

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

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


Способ 1. Программное слияние таблиц «Свернуть -> Добавить -> Свернуть»

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

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

  1. Сворачиваем первую таблицу по ключевым полям, суммируя числовой показатель (например, "Расход"). Это схлопнет дубли внутри первой таблицы.
  2. Сворачиваем вторую таблицу аналогичным образом, чтобы избавиться от дублей внутри нее.
  3. Переносим (добавляем) все строки из второй таблицы в первую.
  4. Снова сворачиваем первую таблицу. Те строки, которые пришли из второй таблицы и совпали с первой по ключевым полям, автоматически объединятся, а их показатели сложатся.

Посмотрим на практическую реализацию этого метода:


// Создадим демонстрационные таблицы значений
ТЗ1 = Новый ТаблицаЗначений;
ТЗ1.Колонки.Добавить("Хар", Новый ОписаниеТипов("Строка"));
ТЗ1.Колонки.Добавить("Цвет", Новый ОписаниеТипов("Строка"));
ТЗ1.Колонки.Добавить("Пластик", Новый ОписаниеТипов("Строка"));
ТЗ1.Колонки.Добавить("Расход", Новый ОписаниеТипов("Число"));

ТЗ2 = ТЗ1.СкопироватьКолонки();

// Наполним ТЗ1 тестовыми данными с дублями внутри таблицы
Стр = ТЗ1.Добавить();
Стр.Хар = "Жесткий"; Стр.Цвет = "Красный"; Стр.Пластик = "Ребристый"; Стр.Расход = 10;

Стр = ТЗ1.Добавить(); // Дублирующая строка для ТЗ1
Стр.Хар = "Жесткий"; Стр.Цвет = "Красный"; Стр.Пластик = "Ребристый"; Стр.Расход = 5;

Стр = ТЗ1.Добавить();
Стр.Хар = "Мягкий"; Стр.Цвет = "Синий"; Стр.Пластик = "Гладкий"; Стр.Расход = 15;

// Наполним ТЗ2 тестовыми данными
Стр = ТЗ2.Добавить(); // Совпадающая строка с ТЗ1
Стр.Хар = "Мягкий"; Стр.Цвет = "Синий"; Стр.Пластик = "Гладкий"; Стр.Расход = 25;

Стр = ТЗ2.Добавить(); // Уникальная строка для ТЗ2
Стр.Хар = "Средний"; Стр.Цвет = "Зеленый"; Стр.Пластик = "Матовый"; Стр.Расход = 8;

// Выполняем алгоритм быстрого слияния
СписокКлючей = "Хар, Цвет, Планшет"; // Опечатка, укажем верные колонки:
СписокКлючей = "Хар, Цвет, Пластик";
СписокРесурсов = "Расход";

// 1. Сворачиваем исходные таблицы по отдельности
ТЗ1.Свернуть(СписокКлючей, СписокРесурсов);
ТЗ2.Свернуть(СписокКлючей, СписокРесурсов);

// 2. Переносим строки из второй таблицы в первую
Для Каждого СтрокаТЗ2 Из ТЗ2 Цикл
    НоваяСтрока = ТЗ1.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ2);
КонецЦикла;

// 3. Финальная свертка объединенного массива строк
ТЗ1.Свернуть(СписокКлючей, СписокРесурсов);

// В результате ТЗ1 будет содержать полностью сгруппированные и суммированные данные

Преимущества метода: невероятно прост в написании, не требует обращения к СУБД, работает быстро на малых и средних объемах данных.

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


Способ 2. Построчное сопоставление через «НайтиСтроки» с предварительной индексацией

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

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

Рассмотрим пример реализации:


// Сворачиваем таблицы для исключения внутренних дублей
ТЗ1.Свернуть("Хар, Цвет, Пластик", "Расход");
ТЗ2.Свернуть("Хар, Цвет, Пластик", "Расход");

// КРИТИЧЕСКИ ВАЖНО: Добавляем индекс по полям поиска для ускорения работы НайтиСтроки
ТЗ1.Индексы.Добавить("Хар, Цвет, Пластик");

СтруктураПоиска = Новый Структура("Хар, Цвет, Пластик");

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

Благодаря строке ТЗ1.Индексы.Добавить() этот цикл будет стабильно и быстро работать даже на таблицах, содержащих десятки тысяч строк.


Способ 3. Оптимальное слияние в памяти через объект «Соответствие» (алгоритм O(N+M))

Если производительность является главным приоритетом, а обращаться к СУБД (запросам) по каким-то причинам нельзя, лучшим выбором будет использование объекта Соответствие. Этот подход позволяет объединить таблицы за линейное время $O(N+M)$, полностью избегая вложенных циклов и повторных поисков по таблице значений.

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


// Создаем результирующую таблицу, куда скопируем только структуру
ТЗ_Результат = ТЗ1.СкопироватьКолонки();

// Хэш-карта для быстрого сопоставления строк
КартаСтрок = Новый Соответствие;

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

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

// На выходе в ТЗ_Результат получаем идеально объединенную таблицу

Данный алгоритм является самым быстрым способом слияния таблиц в оперативной памяти программными средствами 1С.


Способ 4. Использование механизма Запросов (Временные таблицы)

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

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

Разберем наиболее эффективный вариант запроса, основанный на предварительной группировке таблиц, их объединении через оператор ОБЪЕДИНИТЬ ВСЕ и последующей финальной группировке. Этот способ работает быстрее и надежнее, чем ПОЛНОЕ СОЕДИНЕНИЕ, так как исключает появление избыточных значений NULL в результатах соединений.

Посмотрим на пример кода:


// Инициализируем запрос
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;

// Передаем таблицы значений в параметры запроса
Запрос.УстановитьПараметр("Таблица1", ТЗ1);
Запрос.УстановитьПараметр("Таблица2", ТЗ2);

Запрос.Текст = 
"ВЫБРАТЬ
|	ТЗ1.Хар КАК Хар,
|	ТЗ1.Цвет КАК Цвет,
|	ТЗ1.Пластик КАК Пластик,
|	ТЗ1.Расход КАК Расход
|ПОМЕСТИТЬ ВТ_Таблица1
|ИЗ
|	&Таблица1 КАК ТЗ1
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ТЗ2.Хар КАК Хар,
|	ТЗ2.Цвет КАК Цвет,
|	ТЗ2.Пластик КАК Пластик,
|	ТЗ2.Расход КАК Расход
|ПОМЕСТИТЬ ВТ_Таблица2
|ИЗ
|	&Таблица2 КАК ТЗ2
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ1.Хар КАК Хар,
|	ВТ1.Цвет КАК Цвет,
|	ВТ1.Пластик КАК Пластик,
|	СУММА(ВТ1.Расход) КАК Расход,
|	1 КАК Источник // Флаг: строка была в первой таблице
|ПОМЕСТИТЬ ВТ_Сгруппированная1
|ИЗ
|	ВТ_Таблица1 КАК ВТ1
|
|СГРУППИРОВАТЬ ПО
|	ВТ1.Хар,
|	ВТ1.Цвет,
|	ВТ1.Пластик
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ2.Хар КАК Хар,
|	ВТ2.Цвет КАК Цвет,
|	ВТ2.Пластик КАК Пластик,
|	СУММА(ВТ2.Расход) КАК Расход,
|	2 КАК Источник // Флаг: строка была во второй таблице
|ПОМЕСТИТЬ ВТ_Сгруппированная2
|ИЗ
|	ВТ_Таблица2 КАК ВТ2
|
|СГРУППИРОВАТЬ ПО
|	ВТ2.Хар,
|	ВТ2.Цвет,
|	ВТ2.Пластик
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ_Сгр1.Хар КАК Хар,
|	ВТ_Сгр1.Цвет КАК Цвет,
|	ВТ_Сгр1.Пластик КАК Пластик,
|	ВТ_Сгр1.Расход КАК Расход,
|	ВТ_Сгр1.Источник КАК Источник
|ПОМЕСТИТЬ ВТ_Объединенная
|ИЗ
|	ВТ_Сгруппированная1 КАК ВТ_Сгр1
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ
|	ВТ_Сгр2.Хар,
|	ВТ_Сгр2.Цвет,
|	ВТ_Сгр2.Пластик,
|	ВТ_Сгр2.Расход,
|	ВТ_Сгр2.Источник
|ИЗ
|	ВТ_Сгруппированная2 КАК ВТ_Сгр2
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ_Объед.Хар КАК Хар,
|	ВТ_Объед.Цвет КАК Цвет,
|	ВТ_Объед.Пластик КАК Пластик,
|	СУММА(ВТ_Объед.Расход) КАК Расход,
|	// Сумма источников покажет нам происхождение строки:
|	// 1 - только из первой ТЗ, 
|	// 2 - только из второй ТЗ, 
|	// 3 (1+2) - строка найдена в обеих таблицах!
|	СУММА(ВТ_Объед.Источник) КАК СтатусИсточника
|ИЗ
|	ВТ_Объединенная КАК ВТ_Объед
|
|СГРУППИРОВАТЬ ПО
|	ВТ_Объед.Хар,
|	ВТ_Объед.Цвет,
|	ВТ_Объед.Пластик";

// Выполняем запрос и выгружаем результат в итоговую Таблицу Значений
ТЗ_Итог = Запрос.Выполнить().Выгрузить();

Почему этот запрос оптимален?


Резюме: какой способ выбрать?

Для того чтобы сделать правильный архитектурный выбор в вашем конкретном случае, ориентируйтесь на следующие рекомендации:

  1. Если таблицы небольшие (до 1000 строк), а структура колонок у них полностью идентична, используйте простой программный метод «Свернуть -> Добавить -> Свернуть» (Способ 1). Это сэкономит ваше время и обеспечит хорошую читаемость кода.
  2. Если вам нужно выполнить слияние в памяти без обращения к БД, но объемы данных могут быть значительными (десятки тысяч строк), используйте Способ 3 (через объект Соответствие) или Способ 2 (с обязательным использованием Индексов). Это спасет вашу конфигурацию от зависаний.
  3. Если вы работаете в рамках общего серверного вызова, у вас уже открыта транзакция или логика требует дальнейшего соединения полученных данных с физическими таблицами базы данных — смело используйте запросы с временными таблицами (Способ 4). Это наиболее масштабируемое и профессиональное решение.
← На главную