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

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

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

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

Способ 1. Запрос с использованием ПОЛНОГО СОЕДИНЕНИЯ

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

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


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

ЗапросТЗ.УстановитьПараметр("ТЗПредыдущая", ТЗПредыдущая);
ЗапросТЗ.УстановитьПараметр("ТЗТекущая", ТЗТекущая);

ТЗОбъединенная = ЗапросТЗ.Выполнить().Выгрузить();

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

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

Способ 2. Запрос через ОБЪЕДИНИТЬ ВСЕ и группировку

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

Разберем по шагам текст запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	тзПрошлыйПериод.Контрагент КАК Контрагент,
    |	тзПрошлыйПериод.Количество КАК КоличествоЗаПрошлыйПериод
    |ПОМЕСТИТЬ ВТ_ПрошлыйПериод
    |ИЗ
    |	&тзПрошлыйПериод КАК тзПрошлыйПериод
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	тзТекущийПериод.Контрагент КАК Контрагент,
    |	тзТекущийПериод.Количество КАК КоличествоЗаТекущийПериод
    |ПОМЕСТИТЬ ВТ_ТекущийПериод
    |ИЗ
    |	&тзТекущийПериод КАК тзТекущийПериод
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВложенныйЗапрос.Контрагент КАК Контрагент,
    |	СУММА(ВложенныйЗапрос.КоличествоЗаПрошлыйПериод) КАК КоличествоЗаПрошлыйПериод,
    |	СУММА(ВложенныйЗапрос.КоличествоЗаТекущийПериод) КАК КоличествоЗаТекущийПериод
    |ИЗ
    |	(ВЫБРАТЬ
    |		ВТ_ПрошлыйПериод.Контрагент КАК Контрагент,
    |		ВТ_ПрошлыйПериод.КоличествоЗаПрошлыйПериод КАК КоличествоЗаПрошлыйПериод,
    |		0 КАК КоличествоЗаТекущийПериод
    |	ИЗ
    |		ВТ_ПрошлыйПериод КАК ВТ_ПрошлыйПериод
    |	
    |	ОБЪЕДИНИТЬ ВСЕ
    |	
    |	ВЫБРАТЬ
    |		ВТ_ТекущийПериод.Контрагент,
    |		0,
    |		ВТ_ТекущийПериод.КоличествоЗаТекущийПериод
    |	ИЗ
    |		ВТ_ТекущийПериод КАК ВТ_ТекущийПериод) КАК ВложенныйЗапрос
    |
    |СГРУППИРОВАТЬ ПО
    |	ВложенныйЗапрос.Контрагент";

Запрос.УстановитьПараметр("тзПрошлыйПериод", ТЗПредыдущая);
Запрос.УстановитьПараметр("тзТекущийПериод", ТЗТекущая);

тзРезультат = Запрос.Выполнить().Выгрузить();

Этот подход считается наиболее надежным в сложных сценариях, так как автоматически схлопывает дубли и не требует использования функции ЕСТЬNULL для вычисления результирующих числовых колонок.

Способ 3. Платформенный метод Свернуть() в памяти без запроса

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

Рассмотрим порядок действий:

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

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

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

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

// Сворачиваем по контрагенту с суммированием количеств
ТЗОбъединенная.Свернуть("Контрагент", "КоличествоПредыдущее, КоличествоТекущее");

Способ 4. Программный обход с использованием объекта Соответствие

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

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


ТЗОбъединенная = Новый ТаблицаЗначений;
ТЗОбъединенная.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
ТЗОбъединенная.Колонки.Добавить("КоличествоПредыдущее", Новый ОписаниеТипов("Число"));
ТЗОбъединенная.Колонки.Добавить("КоличествоТекущее", Новый ОписаниеТипов("Число"));

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

// Обрабатываем первую таблицу
Для Каждого Стр Из ТЗПредыдущая Цикл
	НовСтр = ТЗОбъединенная.Добавить();
	НовСтр.Контрагент = Стр.Контрагент;
	НовСтр.КоличествоПредыдущее = Стр.Количество;
	
	ЗначениеТекущее = ДанныеТекущие[Стр.Контрагент];
	Если ЗначениеТекущее <> Неопределено Тогда
		НовСтр.КоличествоТекущее = ЗначениеТекущее;
		// Удаляем обработанного контрагента, чтобы не дублировать во втором цикле
		ДанныеТекущие.Удалить(Стр.Контрагент);
	Иначе
		НовСтр.КоличествоТекущее = 0;
	КонецЕсли;
КонецЦикла;

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

Рекомендация по архитектуре решения

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

В тексте единого запроса к регистрам можно сразу настроить периодичность либо сгруппировать обороты за разные периоды через условия ВЫБОР КОГДА ... или ОБЪЕДИНИТЬ ВСЕ. Это избавит сервер от передачи избыточных промежуточных данных в память и повторного создания временных таблиц.

← На главную