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

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

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

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

Способ №1: Последовательный обход с отслеживанием изменения ключа

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

Рассмотрим пример реализации такого обхода:


// Шаг 1. Получаем табличную часть и сортируем её по ключевому полю
ТаблицаОбхода = СсылкаНаОбъект.ТабличнаяЧасть.Выгрузить();
ТаблицаОбхода.Сортировать("НомерЭтапа Возр");

// Шаг 2. Инициализируем переменную для хранения предыдущего значения
НомерПредыдущегоЭтапа = Неопределено;

// Шаг 3. Запускаем цикл обхода
Для Каждого СтрокаТЧ Из ТаблицаОбхода Цикл
    Если СтрокаТЧ.НомерЭтапа <> НомерПредыдущегоЭтапа Тогда
        // Сюда мы попадаем только при смене этапа (появлении нового уникального значения)
        // Выполняем действия по созданию нового элемента справочника
        НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
        НовыйЭлемент.Наименование = "Этап № " + СтрокаТЧ.НомерЭтапа;
        НовыйЭлемент.Записать();
        
        // Фиксируем новый ключ группировки
        НомерПредыдущегоЭтапа = СтрокаТЧ.НомерЭтапа;
    КонецЕсли;
    
    // Здесь можно выполнять обработку детальных строк для текущего этапа,
    // если это необходимо для логики заполнения.
КонецЦикла;

Преимущества: Минимальное потребление оперативной памяти (ОЗУ), отсутствие сложных объектов, высокая скорость выполнения на небольших и средних коллекциях.

Недостатки: Требует обязательной предварительной сортировки. Код может стать перегруженным, если логика обработки детальных записей внутри одной группы достаточно сложна.

Способ №2: Использование метода Свернуть() для простых структур

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

Разберем этот лаконичный алгоритм:


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

// 2. Сворачиваем таблицу по интересующей нас колонке. 
// Все дублирующиеся строки будут удалены, останутся только уникальные.
ТЗОбхода.Свернуть("НомерЭтапа");

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

Преимущества: Очень высокая скорость написания кода и его отличная читаемость. Метод Свернуть реализован на уровне платформы на языке C++ и работает чрезвычайно быстро.

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

Способ №3: Запрос к таблице значений с конструкцией ИТОГИ ПО

Этот подход часто предлагают разработчики, привыкшие решать любые задачи группировки данных с помощью СУБД. Суть метода заключается в передаче таблицы значений в качестве параметра в запрос, помещении её во временную таблицу и выполнении запроса с предложением ИТОГИ ПО.

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


ТЗСпр = ЭлементСправочника.ТабличнаяЧасть.Выгрузить();

Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТЗСпр", ТЗСпр);
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТЗСпр.НомерЭтапа КАК НомерЭтапа,
    |	ТЗСпр.НомерОперации КАК НомерОперации
    |ПОМЕСТИТЬ ВТ_ТабЧасть
    |ИЗ
    |	&ТЗСпр КАК ТЗСпр
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_ТабЧасть.НомерЭтапа КАК НомерЭтапа,
    |	ВТ_ТабЧасть.НомерОперации КАК НомерОперации
    |ИЗ
    |	ВТ_ТабЧасть КАК ВТ_ТабЧасть
    |ИТОГИ ПО
    |	НомерЭтапа";

ВыборкаЭтапов = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаЭтапов.Следующий() Цикл
    // Создаем головной элемент для этапа
    НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
    НовыйЭлемент.Наименование = "Этап № " + ВыборкаЭтапов.НомерЭтапа;
    
    // Получаем подчиненные детальные записи (операции этого этапа)
    ДетальнаяВыборка = ВыборкаЭтапов.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
    Пока ДетальнаяВыборка.Следующий() Цикл
        // Здесь мы можем заполнить подчиненную табличную часть нового элемента
        НоваяСтрока = НовыйЭлемент.Операции.Добавить();
        НоваяСтрока.Операция = ДетальнаяВыборка.НомерОперации;
    КонецЦикла;
    
    НовыйЭлемент.Записать();
КонецЦикла;

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

  1. Платформа 1С выполняет сериализацию таблицы значений из оперативной памяти сервера приложений.
  2. Данные передаются по сети от сервера приложений 1С к СУБД (MS SQL, PostgreSQL и др.).
  3. СУБД создает временную таблицу в своей базе данных (например, в tempdb) и производит физическую запись строк (транзакции INSERT). Это создает нагрузку на жесткие диски сервера СУБД.
  4. СУБД выполняет сортировку и группировку данных.
  5. Результат сериализуется и возвращается обратно по сети на сервер приложений 1С.

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

Когда этот способ оправдан? Использовать запрос оправдано только в двух случаях: во-первых, если объем таблицы превышает десятки тысяч строк (когда мощности СУБД по сортировке начинают превосходить однопоточный встроенный язык 1С); во-вторых, если в запросе выполняется реальное соединение (ЛЕВОЕ СОЕДИНЕНИЕ) с физическими таблицами базы данных (например, для проверки существования элементов в базе данных).

Способ №4: Группировка в памяти через объект Соответствие (Map)

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

Разберем данный алгоритм по шагам:


// Шаг 1. Создаем соответствие для группировки данных в ОЗУ
ГруппировкаПоЭтапам = Новый Соответствие;

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

// Шаг 3. Обходим сгруппированные данные без участия СУБД
Для Каждого ЭлементГруппировки Из ГруппировкаПоЭтапам Цикл
    ТекущийЭтап = ЭлементГруппировки.Ключ;
    ДетальныеСтроки = ЭлементГруппировки.Значение; // Это массив строк ТЧ
    
    // Создаем новый элемент справочника
    НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
    НовыйЭлемент.Наименование = "Этап № " + ТекущийЭтап;
    
    // Наполняем его детальными данными из массива
    Для Каждого СтрокаДеталей Из ДетальныеСтроки Цикл
        НоваяСтрока = НовыйЭлемент.Операции.Добавить();
        НоваяСтрока.Операция = СтрокаДеталей.НомерОперации;
    КонецЦикла;
    
    НовыйЭлемент.Записать();
КонецЦикла;

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

Способ №5: Индексация Таблицы Значений

Еще один профессиональный способ группировки средних объемов данных в оперативной памяти — использование механизма индексирования таблиц значений совместно с методом НайтиСтроки().

Проанализируем пример кода:


// Шаг 1. Выгружаем табличную часть и создаем индекс по группировочной колонке
ТЗ = СсылкаНаОбъект.ТабличнаяЧасть.Выгрузить();
ТЗ.Индексы.Добавить("НомерЭтапа");

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

// Шаг 3. Обходим уникальные ключи и быстро извлекаем детальные записи по индексу
Для Каждого СтрокаЭтапа Из ТЗЭтапы Цикл
    НовыйЭлемент = Справочники.НужныйСправочник.СоздатьЭлемент();
    НовыйЭлемент.Наименование = "Этап № " + СтрокаЭтапа.НомерЭтапа;
    
    // Быстрый поиск строк по индексу (работает практически мгновенно)
    ПараметрыОтбора = Новый Структура("НомерЭтапа", СтрокаЭтапа.НомерЭтапа);
    ДетальныеСтроки = ТЗ.НайтиСтроки(ПараметрыОтбора); // Возвращает массив строк
    
    Для Каждого СтрокаДеталей Из ДетальныеСтроки Цикл
        НоваяСтрока = НовыйЭлемент.Операции.Добавить();
        НоваяСтрока.Операция = СтрокаДеталей.НомерОперации;
    КонецЦикла;
    
    НовыйЭлемент.Записать();
КонецЦикла;

Преимущества: Простой и понятный код. За счет явного добавления индекса в ТЗ.Индексы поиск строк методом НайтиСтроки() выполняется по хэш-таблице, минуя линейный перебор всей коллекции, что обеспечивает высокую скорость работы даже на таблицах в несколько тысяч строк.

Резюме и рекомендации по выбору решения

Подводя итоги, сформулируем практические рекомендации по выбору правильного метода обхода коллекций в вашей повседневной практике программирования в 1С:

← На главную