В процессе разработки отчетов и аналитических обработок в 1С часто возникает задача: объединить две исходные таблицы значений (например, продажи за прошлый и текущий периоды с колонками Контрагент и Количество) в одну результирующую. В итоговой таблице должны присутствовать колонка с контрагентом и две колонки с показателями за соответствующие периоды, причем список контрагентов должен быть полным, а отсутствующие значения должны корректно заполняться нулями.
Разберем основные практические способы решения этой задачи: с помощью языка запросов, платформенного метода Свернуть(), а также программным перебором.
Рассмотрим вариант, при котором обе таблицы передаются в качестве параметров в объект Запрос, помещаются во временные таблицы и соединяются между собой. Чтобы не потерять контрагентов, которые присутствовали только в одном из периодов, используется ПОЛНОЕ СОЕДИНЕНИЕ, а поля оборачиваются в функцию ЕСТЬNULL для замены отсутствующих данных на ноль.
Посмотрим на пример реализации:
ЗапросТЗ = Новый Запрос;
ЗапросТЗ.Текст =
"ВЫБРАТЬ
| ТЗПредыдущая.Контрагент КАК Контрагент,
| ТЗПредыдущая.Количество КАК Количество
|ПОМЕСТИТЬ ТЗПредыдущая
|ИЗ
| &ТЗПредыдущая КАК ТЗПредыдущая
|ИНДЕКСИРОВАТЬ ПО
| Контрагент
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ТЗТекущая.Контрагент КАК Контрагент,
| ТЗТекущая.Количество КАК Количество
|ПОМЕСТИТЬ ТЗТекущая
|ИЗ
| &ТЗТекущая КАК ТЗТекущая
|ИНДЕКСИРОВАТЬ ПО
| Контрагент
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ЕСТЬNULL(ТЗПредыдущая.Контрагент, ТЗТекущая.Контрагент) КАК Контрагент,
| ЕСТЬNULL(ТЗПредыдущая.Количество, 0) КАК КоличествоПредыдущее,
| ЕСТЬNULL(ТЗТекущая.Количество, 0) КАК КоличествоТекущее
|ИЗ
| ТЗПредыдущая КАК ТЗПредыдущая
| ПОЛНОЕ СОЕДИНЕНИЕ ТЗТекущая КАК ТЗТекущая
| ПО ТЗПредыдущая.Контрагент = ТЗТекущая.Контрагент";
ЗапросТЗ.УстановитьПараметр("ТЗПредыдущая", ТЗПредыдущая);
ЗапросТЗ.УстановитьПараметр("ТЗТекущая", ТЗТекущая);
ТЗОбъединенная = ЗапросТЗ.Выполнить().Выгрузить();
Обратим внимание на важный нюанс: при передаче таблицы значений в качестве параметра в запрос все колонки таблицы должны иметь строго определенные типы (объект ОписаниеТипов). Если колонка не типизирована, платформа выдаст ошибку на этапе выполнения запроса.
Кроме того, данный способ корректно работает только в том случае, если контрагенты в каждой исходной таблице уникальны. Если в исходных данных возможны дубли контрагентов, полное соединение приведет к декартову произведению строк.
Проанализируем второй способ работы с запросом, который лишен риска задвоения строк при наличии неуникальных записей. Вместо соединения двух таблиц по ключевому полю мы объединяем их по вертикали с помощью конструкции ОБЪЕДИНИТЬ ВСЕ, подставляя во вторую числовую колонку ноль, а затем выполняем группировку и суммирование.
Разберем по шагам текст запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| тзПрошлыйПериод.Контрагент КАК Контрагент,
| тзПрошлыйПериод.Количество КАК КоличествоЗаПрошлыйПериод
|ПОМЕСТИТЬ ВТ_ПрошлыйПериод
|ИЗ
| &тзПрошлыйПериод КАК тзПрошлыйПериод
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| тзТекущийПериод.Контрагент КАК Контрагент,
| тзТекущийПериод.Количество КАК КоличествоЗаТекущийПериод
|ПОМЕСТИТЬ ВТ_ТекущийПериод
|ИЗ
| &тзТекущийПериод КАК тзТекущийПериод
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВложенныйЗапрос.Контрагент КАК Контрагент,
| СУММА(ВложенныйЗапрос.КоличествоЗаПрошлыйПериод) КАК КоличествоЗаПрошлыйПериод,
| СУММА(ВложенныйЗапрос.КоличествоЗаТекущийПериод) КАК КоличествоЗаТекущийПериод
|ИЗ
| (ВЫБРАТЬ
| ВТ_ПрошлыйПериод.Контрагент КАК Контрагент,
| ВТ_ПрошлыйПериод.КоличествоЗаПрошлыйПериод КАК КоличествоЗаПрошлыйПериод,
| 0 КАК КоличествоЗаТекущийПериод
| ИЗ
| ВТ_ПрошлыйПериод КАК ВТ_ПрошлыйПериод
|
| ОБЪЕДИНИТЬ ВСЕ
|
| ВЫБРАТЬ
| ВТ_ТекущийПериод.Контрагент,
| 0,
| ВТ_ТекущийПериод.КоличествоЗаТекущийПериод
| ИЗ
| ВТ_ТекущийПериод КАК ВТ_ТекущийПериод) КАК ВложенныйЗапрос
|
|СГРУППИРОВАТЬ ПО
| ВложенныйЗапрос.Контрагент";
Запрос.УстановитьПараметр("тзПрошлыйПериод", ТЗПредыдущая);
Запрос.УстановитьПараметр("тзТекущийПериод", ТЗТекущая);
тзРезультат = Запрос.Выполнить().Выгрузить();
Этот подход считается наиболее надежным в сложных сценариях, так как автоматически схлопывает дубли и не требует использования функции ЕСТЬNULL для вычисления результирующих числовых колонок.
Если таблицы уже сформированы в памяти и нет необходимости отправлять их в движок СУБД, мы можем использовать встроенный быстрый метод Свернуть() объекта ТаблицаЗначений. Алгоритм выполняется на уровне платформы и работает значительно быстрее пользовательских циклов.
Рассмотрим порядок действий:
Контрагент, КоличествоПредыдущее и КоличествоТекущее.КоличествоПредыдущее, оставляя текущее значение равным нулю.КоличествоТекущее, оставляя предыдущее равным нулю.Свернуть() по ключевой колонке с суммированием числовых колонок.
// Создаем результирующую таблицу нужной структуры
ТЗОбъединенная = Новый ТаблицаЗначений;
ТЗОбъединенная.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
ТЗОбъединенная.Колонки.Добавить("КоличествоПредыдущее", Новый ОписаниеТипов("Число"));
ТЗОбъединенная.Колонки.Добавить("КоличествоТекущее", Новый ОписаниеТипов("Число"));
// Заполняем данными из прошлой таблицы
Для Каждого Стр Из ТЗПредыдущая Цикл
НовСтр = ТЗОбъединенная.Добавить();
НовСтр.Контрагент = Стр.Контрагент;
НовСтр.КоличествоПредыдущее = Стр.Количество;
НовСтр.КоличествоТекущее = 0;
КонецЦикла;
// Заполняем данными из текущей таблицы
Для Каждого Стр Из ТЗТекущая Цикл
НовСтр = ТЗОбъединенная.Добавить();
НовСтр.Контрагент = Стр.Контрагент;
НовСтр.КоличествоПредыдущее = 0;
НовСтр.КоличествоТекущее = Стр.Количество;
КонецЦикла;
// Сворачиваем по контрагенту с суммированием количеств
ТЗОбъединенная.Свернуть("Контрагент", "КоличествоПредыдущее, КоличествоТекущее");
Иногда требуется объединить таблицы вручную в цикле без вызова механизма запросов. Обычный перебор с вызовом метода ТаблицаЗначений.Найти() на больших объемах данных работает медленно, так как выполняет линейный поиск по всей таблице для каждого элемента.
Выясним, как ускорить этот процесс: для быстрого сопоставления данных рекомендуется использовать объект Соответствие, поиск по ключу в котором работает по хэш-таблице практически мгновенно.
ТЗОбъединенная = Новый ТаблицаЗначений;
ТЗОбъединенная.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
ТЗОбъединенная.Колонки.Добавить("КоличествоПредыдущее", Новый ОписаниеТипов("Число"));
ТЗОбъединенная.Колонки.Добавить("КоличествоТекущее", Новый ОписаниеТипов("Число"));
// Помещаем текущие данные в Соответствие для быстрого поиска: Ключ = Контрагент, Значение = Количество
ДанныеТекущие = Новый Соответствие;
Для Каждого Стр Из ТЗТекущая Цикл
ДанныеТекущие.Вставить(Стр.Контрагент, Стр.Количество);
КонецЦикла;
// Обрабатываем первую таблицу
Для Каждого Стр Из ТЗПредыдущая Цикл
НовСтр = ТЗОбъединенная.Добавить();
НовСтр.Контрагент = Стр.Контрагент;
НовСтр.КоличествоПредыдущее = Стр.Количество;
ЗначениеТекущее = ДанныеТекущие[Стр.Контрагент];
Если ЗначениеТекущее <> Неопределено Тогда
НовСтр.КоличествоТекущее = ЗначениеТекущее;
// Удаляем обработанного контрагента, чтобы не дублировать во втором цикле
ДанныеТекущие.Удалить(Стр.Контрагент);
Иначе
НовСтр.КоличествоТекущее = 0;
КонецЕсли;
КонецЦикла;
// Добавляем контрагентов, которые были только во втором периоде
Для Каждого КлючИЗначение Из ДанныеТекущие Цикл
НовСтр = ТЗОбъединенная.Добавить();
НовСтр.Контрагент = КлючИЗначение.Ключ;
НовСтр.КоличествоПредыдущее = 0;
НовСтр.КоличествоТекущее = КлючИЗначение.Значение;
КонецЦикла;
С точки зрения производительности системы объединение таблиц в памяти — это устранение последствий неоптимального получения данных. Если обе таблицы формируются на основе данных из базы (например, из регистра накопления продаж), правильнее получать итоговый результат одним запросом к базе данных.
В тексте единого запроса к регистрам можно сразу настроить периодичность либо сгруппировать обороты за разные периоды через условия ВЫБОР КОГДА ... или ОБЪЕДИНИТЬ ВСЕ. Это избавит сервер от передачи избыточных промежуточных данных в память и повторного создания временных таблиц.