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