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