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