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