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

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

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

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

Способ 1. Локальный запрос к таблице значений (Рекомендуемый стандарт)

Самым надежным, декларативным и простым в поддержке способом является использование временных таблиц в локальном запросе. Платформа 1С позволяет передавать таблицу значений в качестве параметра запроса и выполнять над ней любые стандартные операции SQL, включая агрегатные функции МИНИМУМ, МАКСИМУМ, СРЕДНЕЕ и СУММА.

Рассмотрим этот метод на практическом примере. Представим, что у нас есть таблица со столбцами Номенклатура, СуммаНДС и НомерСтроки. Нам нужно сгруппировать данные по номенклатуре, просуммировать НДС, а для номера строки определить минимальное значение.

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

  1. Убедимся, что у колонок исходной таблицы значений жестко определены типы данных (ОписаниеТипов). Это критически важно, так как процессор запросов 1С не сможет обработать колонки с неопределенным типом.
  2. Создадим локальный запрос и инициализируем менеджер временных таблиц.
  3. Передадим нашу таблицу в качестве параметра и напишем текст запроса с группировкой.

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


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

// Создаем новый запрос для обработки таблицы значений в оперативной памяти
ЗапросЛокальный = Новый Запрос;
ЗапросЛокальный.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;

// Передаем таблицу значений в качестве параметра
ЗапросЛокальный.УстановитьПараметр("ВнешняяТЗ", ИсходнаяТЗ);

// Формируем текст запроса с применением агрегатных функций
ЗапросЛокальный.Текст = 
    "ВЫБРАТЬ
    |	ВнешняяТЗ.Номенклатура КАК Номенклатура,
    |	ВнешняяТЗ.НомерСтроки КАК НомерСтроки,
    |	ВнешняяТЗ.СуммаНДС КАК СуммаНДС
    |ПОМЕСТИТЬ ВТ_ИсходныеДанные
    |ИЗ
    |	&ВнешняяТЗ КАК ВнешняяТЗ
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_ИсходныеДанные.Номенклатура КАК Номенклатура,
    |	МИНИМУМ(ВТ_ИсходныеДанные.НомерСтроки) КАК НомерСтроки,
    |	СУММА(ВТ_ИсходныеДанные.СуммаНДС) КАК СуммаНДС
    |ИЗ
    |	ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
    |
    |СГРУППИРОВАТЬ ПО
    |	ВТ_ИсходныеДанные.Номенклатура";

// Выполняем запрос и выгружаем результат в новую свернутую таблицу значений
СвернутаяТЗ = ЗапросЛокальный.Выполнить().Выгрузить();

Преимущества метода: Код легко читать и модифицировать. Мы можем использовать любые агрегатные функции СУБД, строить сложные связи и условия. Этот способ работает достаточно быстро даже на больших объемах данных.

Способ 2. Алгоритм «Сортировка + Обнуление + Свернуть» (Быстрый трюк без запросов)

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

Разберем логику метода на примере поиска МИНИМУМА:

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

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


// Шаг 1. Сортируем таблицу: группировка — по алфавиту, число минимума — по возрастанию
ТЗ.Сортировать("Номенклатура Возр, НомерСтроки Возр"); 

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

// Шаг 3. Теперь стандартная свертка корректно сложит нули с минимальным числом
ТЗ.Свернуть("Номенклатура", "СуммаНДС, НомерСтроки");

Важное примечание: Если вам необходимо найти **МАКСИМУМ**, измените направление сортировки целевой колонки на убывание (НомерСтроки Убыв). В этом случае максимальное значение окажется первым в группе, а все последующие дубли также заменятся нулями и не исказят результат при последующем суммировании.

Преимущества метода: Невероятная скорость выполнения в оперативной памяти и отсутствие затрат на создание СУБД-контекста.

Способ 3. Ручной обход коллекции с использованием объекта «Соответствие»

Когда требуется реализовать нетривиальную логику свертки (например, одновременно рассчитать среднее значение, медиану, сконкатенировать строки или применить разные условия для разных групп товаров), на помощь приходит классический ручной обход с использованием хэш-таблицы. В 1С роль эффективной хэш-таблицы выполняет объект Соответствие.

Использование метода ТаблицаЗначений.Найти() внутри цикла обхода дает квадратичную сложность алгоритма O(N²), что приведет к зависанию системы на больших таблицах. Использование объекта Соответствие гарантирует близкую к линейной скорость работы O(N) за счет быстрого поиска по ключу.

Выясним, как выглядит реализация этого паттерна на практике:


// Создаем новую чистую таблицу для хранения свернутого результата
ТЗРезультат = Новый ТаблицаЗначений;
ТЗРезультат.Колонки.Добавить("Номенклатура");
ТЗРезультат.Колонки.Добавить("СуммаНДС", Новый ОписаниеТипов("Число"));
ТЗРезультат.Колонки.Добавить("НомерСтроки", Новый ОписаниеТипов("Число"));

// Создаем хэш-таблицу для быстрого сопоставления Номенклатуры и строк результирующей ТЗ
ИндексыГруппировки = Новый Соответствие;

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

Преимущества метода: Абсолютная гибкость. Вы можете прямо внутри условия Иначе описывать любые математические и логические алгоритмы обработки строк, собирать массивы подчиненных элементов или вычислять средневзвешенные показатели.

Способ 4. Использование Системы компоновки данных (СКД)

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

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

Особенности разработки в Управляемом приложении

Важно помнить о клиент-серверной архитектуре современных конфигураций 1С. Объект ТаблицаЗначений доступен исключительно на стороне сервера. Если вы работаете с табличной частью на форме (тип ДанныеФормыКоллекция) на стороне клиента, вам потребуется:

  1. Передать данные на сервер в процедуру с контекстом или без.
  2. Если используется контекстный вызов, преобразовать коллекцию формы в физическую таблицу значений с помощью метода РеквизитВЗначение().
  3. Произвести сворачивание любым из вышеописанных способов.
  4. Вернуть результат обратно на форму, применив метод ЗначениеВРеквизит().

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

← На главную