Как получить самое частое значение в табличной части документа или справочника в 1С?

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

В процессе разработки бизнес-логики на платформе 1С:Предприятие часто возникает задача анализа данных, хранящихся в табличных частях документов или справочников. Типичный пример — голосование сотрудников на форме, когда в табличной части фиксируются ответы («Да», «Нет», «Не знаю»), и системе требуется оперативно вычислить наиболее популярное (частое) решение. В математической статистике такое значение называют модой.

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

Метод 1. Использование запроса с временными таблицами (универсальный и надежный способ)

Если нам необходимо получить наиболее частое значение непосредственно из базы данных, оптимальным решением будет использование механизма запросов. Обычный выбор конструкции ВЫБРАТЬ ПЕРВЫЕ 1 с сортировкой по убыванию количества не подходит, так как в случае равенства голосов (например, два ответа «Да» и два ответа «Нет») мы потеряем одного из лидеров.

Чтобы решить проблему «ничьей» и вернуть все лидирующие значения, мы построим пакетный запрос из трех временных таблиц. Рассмотрим этот алгоритм по шагам:

  1. Группировка и подсчет повторений: На первом шаге мы сгруппируем строки табличной части по целевому значению и посчитаем количество их вхождений с помощью агрегатной функции СУММА(1) или КОЛИЧЕСТВО(). Результат поместим во временную таблицу.
  2. Определение максимального количества: Во втором запросе пакета мы выберем максимальное число повторений из первой временной таблицы.
  3. Фильтрация победителей: В финальном запросе мы выберем только те значения из первой временной таблицы, количество повторений которых равно вычисленному максимуму.

Посмотрим на практический пример реализации такого запроса на встроенном языке 1С:


// СсылкаНаДокумент - ссылка на анализируемый документ или справочник
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТЧ.Решение КАК Решение,
    |	СУММА(1) КАК Количество
    |ПОМЕСТИТЬ ВТ_КоличествоПовторений
    |ИЗ
    |	Документ.ОпросСотрудников.Ответы КАК ТЧ
    |ГДЕ
    |	ТЧ.Ссылка = &Ссылка
    |	И ТЧ.Решение <> ЗНАЧЕНИЕ(Перечисление.ВариантыРешений.ПустаяСсылка)
    |СГРУППИРОВАТЬ ПО
    |	ТЧ.Решение
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	МАКСИМУМ(ВТ.Количество) КАК МаксимальноеКоличество
    |ПОМЕСТИТЬ ВТ_МаксимальноеКоличество
    |ИЗ
    |	ВТ_КоличествоПовторений КАК ВТ
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	КП.Решение КАК Решение,
    |	КП.Количество КАК Количество
    |ИЗ
    |	ВТ_КоличествоПовторений КАК КП
    |ГДЕ
    |	КП.Количество В
    |			(ВЫБРАТЬ
    |				МК.МаксимальноеКоличество
    |			ИЗ
    |				ВТ_МаксимальноеКоличество КАК МК)";

Запрос.УстановитьПараметр("Ссылка", СсылкаНаДокумент);
РезультатЗапроса = Запрос.Выполнить();

Если Не РезультатЗапроса.Пустой() Тогда
    Выборка = РезультатЗапроса.Выбрать();
    МассивРешений = Новый Массив;
    Пока Выборка.Следующий() Цикл
        МассивРешений.Добавить(Строка(Выборка.Решение));
    КонецЦикла;
    
    // Объединяем результаты в одну строку для вывода на форму через запятую
    ИтоговоеСтроковоеЗначение = СтрСоединить(МассивРешений, ", ");
    Сообщить("Лидирующие ответы: " + ИтоговоеСтроковоеЗначение);
КонецЕсли;

Важный нюанс: Обратите внимание на условие ГДЕ ТЧ.Решение <> ЗНАЧЕНИЕ(Перечисление.ВариантыРешений.ПустаяСсылка). Обязательно исключайте пустые ссылки и незаполненные значения из выборки, иначе пустые строки табличной части могут ошибочно выиграть «голосование».

Метод 2. Программная свертка таблицы значений (работа в оперативной памяти)

Если объект уже прочитан и находится в памяти сервера (например, при обработке события ПередЗаписью в модуле объекта), выполнять дополнительный запрос к базе данных может быть неоптимально. Быстрее выгрузить табличную часть в объект ТаблицаЗначений, свернуть ее и отсортировать.

Проанализируем пример кода, реализующий данную логику:


// Выгружаем ТЧ документа в Таблицу Значений
ТЗ = СсылкаНаДокумент.Ответы.Выгрузить();

// Добавляем числовую колонку-счетчик для последующей свертки
ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ТЗ.ЗаполнитьЗначения(1, "Количество");

// Сворачиваем таблицу по исследуемой колонке, суммируя счетчик
ТЗ.Свернуть("Решение", "Количество");

// Сортируем полученную таблицу по убыванию количества повторений
ТЗ.Сортировать("Количество Убыв");

// Выделяем лидеров голосования с учетом возможных равных результатов
Если ТЗ.Количество() > 0 Тогда
    МаксимальноеЧислоГолосов = ТЗ[0].Количество;
    Победители = Новый Массив;
    
    Для Каждого СтрокаТЗ Из ТЗ Цикл
        Если СтрокаТЗ.Количество = МаксимальноеЧислоГолосов Тогда
            Победители.Добавить(СтрокаТЗ.Решение);
        Иначе
            // Так как таблица отсортирована, при первом уменьшении счетчика выходим из цикла
            Прервать;
        EndIf;
    КонецЦикла;
    
    РезультатТекстом = СтрСоединить(Победители, " / ");
КонецЕсли;

Этот метод чрезвычайно прост в отладке и не требует обращения к СУБД, что снижает нагрузку на SQL-сервер при обработке небольших объемов данных.

Метод 3. Быстрый обход через структуру «Соответствие» (Map)

Для ультра-быстрой обработки микро-объемов данных (когда в табличной части всего до 10-20 строк) накладные расходы на создание структуры ТаблицаЗначений или выполнение запроса могут оказаться избыточными. В этом случае мы можем применить обход коллекции циклом с использованием объекта Соответствие.

Разберем по шагам логику этого алгоритма:


СчетчикРешений = Новый Соответствие;

// Шаг 1. Заполнение соответствия парами "Значение - Количество"
Для Каждого СтрокаТЧ Из Объект.Ответы Цикл
    ТекущееЗначение = СтрокаТЧ.Решение;
    Если Не ЗначениеЗаполнено(ТекущееЗначение) Тогда
        Продолжить;
    КонецЕсли;
    
    КоличествоПовторов = СчетчикРешений.Получить(ТекущееЗначение);
    Если КоличествоПовторов = Неопределено Тогда
        СчетчикРешений.Вставить(ТекущееЗначение, 1);
    Иначе
        СчетчикРешений.Вставить(ТекущееЗначение, КоличествоПовторов + 1);
    КонецЕсли;
КонецЦикла;

// Шаг 2. Поиск элементов с максимальным количеством повторений
МаксимальныйПоказатель = 0;
СписокЛидеров = Новый Массив;

Для Каждого ЗаписьСоответствия Из СчетчикРешений Цикл
    Если ЗаписьСоответствия.Значение > МаксимальныйПоказатель Тогда
        МаксимальныйПоказатель = ЗаписьСоответствия.Значение;
        СписокЛидеров.Очистить();
        СписокЛидеров.Добавить(ЗаписьСоответствия.Ключ);
    ИначеЕсли ЗаписьСоответствия.Значение = МаксимальныйПоказатель Тогда
        СписокЛидеров.Добавить(ЗаписьСоответствия.Ключ);
    КонецЕсли;
КонецЦикла;

Использование Соответствие обеспечивает максимальное быстродействие за счет отсутствия системных вызовов СУБД и работы со сложными метаданными таблиц.

Оптимизация для динамических списков: как избежать фатальных зависаний

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

Для каждого отображаемого документа система начнет отправлять индивидуальные запросы в базу данных, многократно увеличивая сетевой трафик и нагружая СУБД (проблема N+1 запроса).

Рекомендованный паттерн проектирования:

  1. Создайте в документе полноценный физический реквизит РезультатГолосования с типом Строка (или ссылкой на соответствующее перечисление/справочник, если победитель всегда один).
  2. В модуле объекта документа в обработчике события ПередЗаписью разместите код вычисления самого частого значения (используя Метод 2 или Метод 3). Вычисленный результат записывайте в этот новый реквизит.
  3. Выведите реквизит РезультатГолосования в форму списка. Теперь платформа будет мгновенно считывать готовое значение прямо из таблицы базы данных без каких-либо дополнительных расчетов. Более того, пользователи получат возможность быстро сортировать и фильтровать список документов по данному полю.

Применение этого несложного правила позволит вашей конфигурации работать плавно и быстро даже на базах данных объемом в несколько терабайт.

← На главную