Как подсчитать количество различных значений в группировке динамического списка?

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

Динамический список в платформе 1С:Предприятие 8 — это мощный инструмент для быстрого и удобного отображения больших массивов информации. Благодаря порционной выборке данных он обеспечивает высокую скорость работы интерфейса. Однако у этой медали есть обратная сторона: динамический список не является полноценным отчетом. Из-за этого в нем стандартно не поддерживается расчет сложных агрегатных функций (таких как расчет количества различных значений по определенному полю) непосредственно в строках группировок, как это легко делается в Системе компоновки данных (СКД).

В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем архитектурные ограничения платформы и рассмотрим три эффективных практических метода, которые позволяют обойти это ограничение и вывести количество различных значений в динамическом списке, а также альтернативный вариант с использованием дерева значений.

В чем сложность задачи?

Когда мы работаем с обычным отчетом на СКД, мы можем назначить любому полю роль ресурса и указать выражение вида Количество(Различные ИмяПоля). Платформа сама позаботится о расчете и выводе итогов по всем уровням группировки. Но если мы попытаемся программно или интерактивно настроить группировку в объекте ДинамическийСписок, то обнаружим, что у него просто нет механизма ресурсов.

Попытки разработчиков решить эту задачу «в лоб» часто приводят к ошибкам или критическому падению производительности системы. Давайте пошагово разберем правильные архитектурные решения этой проблемы.

---

Решение 1. Использование произвольного запроса со вложенной таблицей

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

Рассмотрим конкретный пример. Представим, что у нас есть список документов «Реализация товаров и услуг». Нам необходимо сгруппировать список по полю Контрагент и для каждой группы вывести количество различных (уникальных) товаров, которые этот контрагент приобретал.

Для реализации этого метода выполним следующие шаги:

  1. Откроем свойства динамического списка и установим флаг Произвольный запрос.
  2. Перейдем в настройки запроса и напишем текст, использующий левое соединение со вложенной таблицей агрегации.

Проанализируем пример текста запроса для динамического списка:


ВЫБРАТЬ
    ДокументРеализация.Ссылка КАК Ссылка,
    ДокументРеализация.ПометкаУдаления КАК ПометкаУдаления,
    ДокументРеализация.Номер КАК Номер,
    ДокументРеализация.Дата КАК Дата,
    ДокументРеализация.Проведен КАК Проведен,
    ДокументРеализация.Контрагент КАК Контрагент,
    ДокументРеализация.СуммаДокумента КАК СуммаДокумента,
    ТаблицаНоменклатуры.КоличествоРазличныхТоваров КАК КоличествоРазличныхТоваров
ИЗ
    Документ.РеализацияТоваровУслуг КАК ДокументРеализация
    ЛЕВОЕ СОЕДИНЕНИЕ (
        ВЫБРАТЬ
            ВложенныйДокумент.Контрагент КАК Контрагент,
            КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ТабличнаяЧастьТовары.Номенклатура) КАК КоличествоРазличныхТоваров
        ИЗ
            Документ.РеализацияТоваровУслуг.Товары КАК ТабличнаяЧастьТовары
            ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК ВложенныйДокумент
                ПО ТабличнаяЧастьТовары.Ссылка = ВложенныйДокумент.Ссылка
        ГРУППИРОВАТЬ ПО
            ВложенныйДокумент.Контрагент
    ) КАК ТаблицаНоменклатуры ПО ДокументРеализация.Контрагент = ТаблицаНоменклатуры.Контрагент

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

Плюсы метода: Данные рассчитываются на уровне СУБД, колонка отображается непосредственно в строках группировок списка стандартными средствами платформы.

Минусы метода: На базах данных с миллионами записей подзапрос с конструкцией КОЛИЧЕСТВО(РАЗЛИЧНЫЕ) может вызывать существенные задержки при прокрутке списка (scroll) и динамическом обновлении экрана, так как СУБД придется выполнять тяжелое сканирование таблиц при каждом обращении к БД.

---

Решение 2. Программный расчет через механизмы СКД

Если нам нужно рассчитать общее количество различных значений не для каждой отдельной строки, а вывести итоговый показатель по всему списку (с учетом всех интерактивных отборов, которые установил пользователь), то наилучшим решением будет использование встроенных методов компоновки данных динамического списка.

Для этого мы воспользуемся методами ПолучитьИсполняемуюСхемуКомпоновкиДанных() и ПолучитьИсполняемыеНастройкиКомпоновкиДанных(). Этот подход позволяет перехватить текущие настройки отбора, сортировки и поиска, примененные пользователем к динамическому списку на форме, выполнить программный расчет на сервере и вывести результат в отдельное поле на форме (например, в декорацию или надпись).

Давайте рассмотрим по шагам, как написать такую функцию на сервере:


&НаСервере
Функция РассчитатьКоличествоРазличныхНаСервере() Экспорт

    // 1. Получаем исполняемую схему и настройки динамического списка
    СхемаКомпоновки = Элементы.Список.ПолучитьИсполняемуюСхемуКомпоновкиДанных();
    НастройкиКомпоновки = Элементы.Список.ПолучитьИсполняемыеНастройкиКомпоновкиДанных();
    
    // 2. Инициализируем компоновщик макета
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновки, НастройкиКомпоновки, , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
    
    // 3. Выполняем компоновку данных
    ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновки.Инициализировать(МакетКомпоновки);
    
    // 4. Выводим результат в таблицу значений
    ТаблицаРезультата = Новый ТаблицаЗначений;
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
    ПроцессорВывода.УстановитьОбъект(ТаблицаРезультата);
    ПроцессорВывода.Вывести(ПроцессорКомпоновки);
    
    // 5. Используем возможности ТаблицыЗначений для быстрого подсчета различных
    // Сворачиваем таблицу по интересующему нас полю, чтобы остались только уникальные записи
    ТаблицаРезультата.Свернуть("Контрагент");
    
    Возврат ТаблицаРезультата.Количество();

КонецФункции

Этот код можно вызывать, например, при изменении отборов на форме, при активации строки или по нажатию специальной кнопки «Обновить итоги». Таким образом, пользователь всегда будет видеть точное количество уникальных элементов, соответствующих его текущему поисковому запросу.

---

Решение 3. Использование события «ПриПолученииДанныхНаСервере»

Если вы работаете на платформе 1С:Предприятие 8.3.10 и выше, вам доступен один из самых производительных и элегантных способов решения задачи. Мы можем использовать обработчик события динамического списка ПриПолученииДанныхНаСервере.

Суть метода заключается в том, что в запросе динамического списка мы создаем пустую колонку-буфер (например, ВЫРАЗИТЬ(NULL КАК ЧИСЛО) КАК КоличествоРазличных). А затем, когда платформа считывает порцию данных для отображения на экране (обычно это 20-40 строк), мы на лету точечно рассчитываем агрегатное значение в базе данных только для этих видимых строк и заполняем буферную колонку.

Разберем программную реализацию этого метода:


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

КонецПроцедуры

Преимущество этого метода колоссально: База данных рассчитывает агрегатные функции мгновенно, потому что выборка ограничена жестким фильтром по массиву отображаемых строк на форме. Это полностью исключает зависания интерфейса пользователя.

---

Альтернатива. Отказ от Динамического списка в пользу Дерева значений

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

Вместо этого мы рекомендуем использовать следующую схему работы:

  1. Создайте на форме реквизит с типом ДеревоЗначений и свяжите его с табличным полем на форме.
  2. Напишите стандартный классический запрос 1С, рассчитывающий все необходимые итоги по группировкам на стороне СУБД.
  3. Выгрузите результат выполнения запроса в дерево значений методом Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам).
  4. Отобразите полученное дерево значений на форме.

Этот подход гарантирует 100% стабильность работы, предсказуемое время отклика интерфейса и отсутствие скрытых проблем с оптимизацией запросов СУБД, которые часто возникают при попытках расширить функционал динамического списка.

← На главную