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