Как перенести строки таблицы значений в дерево с группировкой и вложенностью

· 9 мин

Содержание

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

Выбор зависит от того, откуда берётся вложенность. Если уровни задают значения колонок (контрагент → договор → документ), подходит группировка или запрос. Если иерархия хранится в самих данных (группы справочника), нужна рекурсия. Всё ниже касается платформы 1С 8.3 и любой конфигурации: это встроенный язык, а не прикладной механизм.

Чем таблица значений отличается от дерева значений

Обе коллекции живут только в памяти и работают на сервере или в толстом клиенте. В тонком клиенте и в веб-клиенте ни таблицы, ни дерева значений нет: на форме вместо них используются ДанныеФормыКоллекция и ДанныеФормыДерево.

ВозможностьТаблицаЗначенийДеревоЗначений
Строкиодна плоская коллекцияу каждой строки своя коллекция Строки
Ссылка на родителянетсвойство Родитель
Номер уровнянетметод Уровень() у строки
Индексы для поискаИндексы.Добавить()нет
Свёртка с суммированиемСвернуть()нет
Параметр запросаможно передатьнельзя
Реквизит управляемой формыДанныеФормыКоллекцияДанныеФормыДерево

Из таблицы видно главное ограничение. Всю подготовку данных (свёртку, сортировку, поиск по индексу) удобнее сделать ещё в таблице значений, а дерево строить в самом конце.

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

Простое копирование: те же колонки, те же строки

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

Функция ДеревоПоСтруктуреТаблицы(ТЗ) Экспорт

    Дерево = Новый ДеревоЗначений;
    Для Каждого Колонка Из ТЗ.Колонки Цикл
        Дерево.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения, Колонка.Заголовок, Колонка.Ширина);
    КонецЦикла;

    Возврат Дерево;

КонецФункции

Дерево = ДеревоПоСтруктуреТаблицы(ТЗ);
Для Каждого СтрокаТЗ Из ТЗ Цикл
    ЗаполнитьЗначенияСвойств(Дерево.Строки.Добавить(), СтрокаТЗ);
КонецЦикла;

Что здесь происходит:

  1. Колонки дерева создаются с теми же ТипЗначения. Это не формальность: без типа новая строка получит в числовой колонке Неопределено, а не 0, и суммирование в группах упадёт.
  2. ЗаполнитьЗначенияСвойств переносит значения по совпадающим именам колонок. Колонки, которых нет в приёмнике, просто пропускаются.
  3. Если нужны не все колонки, передайте третий параметр строкой: ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ, "Номенклатура, Количество").

Функцию ДеревоПоСтруктуреТаблицы используют и дальше, во всех остальных алгоритмах.

Группировка при переносе в дерево значений

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

Классический приём «отсортировать и ловить смену значения» работает, но у него есть подвох. Сортировка ссылок в таблице значений не обязана ставить одинаковые ссылки подряд, если у разных элементов совпадает представление. Надёжнее хранить уже созданные группы в Соответствие: ключ — значение поля, значение — строка дерева. Тогда порядок исходных строк не важен, а каждая строка таблицы обрабатывается за один проход.

// ПоляГруппировки: "Контрагент, Договор"; ПоляСумм: "Сумма"
Функция ДеревоСГруппировкой(ТЗ, ПоляГруппировки, ПоляСумм) Экспорт

    Дерево = ДеревоПоСтруктуреТаблицы(ТЗ);
    Группы = СтрРазделить(СтрЗаменить(ПоляГруппировки, " ", ""), ",", Ложь);
    Суммы  = СтрРазделить(СтрЗаменить(ПоляСумм, " ", ""), ",", Ложь);

    // Для каждого уровня - строка с перечнем полей, которые надо заполнить в группе
    ПоляУровня = Новый Массив;
    Накоплено = "";
    Для Каждого ИмяПоля Из Группы Цикл
        Накоплено = Накоплено + ?(Накоплено = "", "", ",") + ИмяПоля;
        ПоляУровня.Добавить(Накоплено);
    КонецЦикла;

    КэшГрупп = Новый Соответствие; // родитель -> Соответствие(значение поля -> строка-группа)

    Для Каждого СтрокаТЗ Из ТЗ Цикл

        Родитель = Дерево;
        Для Инд = 0 По Группы.ВГраница() Цикл

            Дети = КэшГрупп[Родитель];
            Если Дети = Неопределено Тогда
                Дети = Новый Соответствие;
                КэшГрупп.Вставить(Родитель, Дети);
            КонецЕсли;

            Значение = СтрокаТЗ[Группы[Инд]];
            Группа = Дети[Значение];
            Если Группа = Неопределено Тогда
                Группа = Родитель.Строки.Добавить();
                ЗаполнитьЗначенияСвойств(Группа, СтрокаТЗ, ПоляУровня[Инд]);
                Дети.Вставить(Значение, Группа);
            КонецЕсли;

            Для Каждого ИмяСуммы Из Суммы Цикл
                Группа[ИмяСуммы] = Группа[ИмяСуммы] + СтрокаТЗ[ИмяСуммы];
            КонецЦикла;

            Родитель = Группа;

        КонецЦикла;

        ЗаполнитьЗначенияСвойств(Родитель.Строки.Добавить(), СтрокаТЗ);

    КонецЦикла;

    Возврат Дерево;

КонецФункции

Пример с числами. В таблице три строки: «ООО Альфа / Договор 1 / 1 000», «ООО Альфа / Договор 2 / 2 500», «ООО Бета / Договор 7 / 400». Дерево получится таким: «ООО Альфа» с суммой 3 500 и двумя договорами (1 000 и 2 500), «ООО Бета» с суммой 400 и одним договором. Каждый договор содержит свою строку-документ.

Несколько деталей, которые легко упустить:

  • 🔹 В строке группы заполняются только поля её уровня и выше. У группы «Договор» стоят и контрагент, и договор, у группы «Контрагент» — только контрагент. Так дерево удобно фильтровать и выводить.
  • 🔹 Ключом Соответствие может быть и сама строка дерева, и объект ДеревоЗначений. Поэтому один кэш обслуживает все уровни сразу.
  • 🔹 Если в группе нужны не суммы, а количество документов, добавьте в таблицу числовую колонку со значением 1 и укажите её в ПоляСумм.

Если детальные строки не нужны, а нужны только итоги по нижнему уровню, сверните таблицу заранее: ТЗ.Свернуть("Контрагент, Договор", "Сумма"). Строк станет меньше, и последняя строка цикла ЗаполнитьЗначенияСвойств(Родитель.Строки.Добавить(), СтрокаТЗ) можно убрать.

Дерево значений через запрос с ИТОГИ

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

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

Запрос.УстановитьПараметр("ТаблицаДанных", ТЗ);
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Структура результата: верхний уровень — контрагенты с итогом, под ними договоры с итогом, под договорами детальные записи.

Чем этот способ отличается от цикла:

КритерийЦикл с СоответствиеЗапрос с ИТОГИ
Где работаетсервер, толстый клиенттолько сервер
Требования к колонкамжелательно с типамитипы обязательны, иначе запрос не выполнится
Поля групп, не входящие в итогизаполняются по вашему кодув строках групп будут NULL
Агрегатылюбые, пишете самиСУММА, КОЛИЧЕСТВО, МИНИМУМ, МАКСИМУМ, СРЕДНЕЕ
Иерархия справочникане учитываетключевое слово ИЕРАРХИЯ в ПО
Дополнительные данные из базыотдельный запроссоединение прямо во втором пакете

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

Если данные в таблицу изначально пришли из запроса, лучше вообще не выгружать их в таблицу значений, а сразу добавить ИТОГИ ПО в исходный запрос. Лишний круг через память уходит.

Рекурсия: когда иерархия хранится в полях Ссылка и Родитель

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

Процедура ДобавитьПодчиненные(СтрокиПриемник, ТЗ, ТекущийРодитель, Счетчик)

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

КонецПроцедуры

ТЗ.Индексы.Добавить("Родитель");
Дерево = ДеревоПоСтруктуреТаблицы(ТЗ);
Счетчик = 0;
ДобавитьПодчиненные(Дерево.Строки, ТЗ, Справочники.Номенклатура.ПустаяСсылка(), Счетчик);

Пояснения к коду:

  1. Корень дерева — строки с пустым родителем. Для справочника это ПустаяСсылка() нужного менеджера; для строковых кодов — пустая строка "".
  2. Индекс по колонке Родитель обязателен. Без него каждый вызов НайтиСтроки перебирает всю таблицу, и на больших объёмах время растёт квадратично.
  3. Счетчик считает перенесённые строки. Его сравнивают с ТЗ.Количество() после построения (подробнее в разделе о проверках).
  4. Номер уровня в дереве хранить отдельно не нужно: его возвращает НоваяСтрока.Уровень(), верхний уровень равен 0.

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

Если таблица получена запросом к иерархическому справочнику, рекурсия часто не нужна: ИТОГИ ПО Ссылка ИЕРАРХИЯ и выгрузка ОбходРезультатаЗапроса.ПоГруппировкамСИерархией дают то же дерево силами платформы.

Какой алгоритм выбрать?

Большие таблицы: что ускоряет перенос

На десятках строк любой алгоритм работает мгновенно. Разница появляется на десятках тысяч строк, и почти всегда она в поиске.

  • 🚀 Не ищите группу в дереве. Строки.Найти() с поиском по подчинённым на каждой итерации — квадратичный алгоритм. Кэш в Соответствие даёт поиск по ключу.
  • 🚀 Индексируйте таблицу до рекурсии. Одна строка ТЗ.Индексы.Добавить("Родитель") превращает перебор в поиск по индексу.
  • 🚀 Сворачивайте заранее. Свернуть() в таблице значений уменьшает число строк, которые попадут в дерево.
  • 🚀 Убирайте лишние колонки. ТЗ.Скопировать(, "Контрагент, Договор, Сумма") создаёт таблицу только с нужными полями, дерево станет легче.
  • 🚀 Стройте дерево одним серверным вызовом. Перенос на форму — ЗначениеВРеквизитФормы(Дерево, "ДеревоДанных") в серверной процедуре формы. Каждый лишний переход клиент-сервер передаёт данные заново.

Для очень больших деревьев на форме не выводите все уровни сразу. Загрузите верхний уровень, а подчинённые строки подгружайте в обработчике события ПередРазворачиванием таблицы формы. Пользователь всё равно раскрывает несколько веток, а не все.

Проверки до и после построения дерева

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

До построения:

  1. Наличие колонок. ТЗ.Колонки.Найти("Договор") = Неопределено означает, что группировать не по чему. Лучше сразу вызвать исключение с понятным текстом, чем получить ошибку обращения к полю в середине цикла.
  2. Типы числовых колонок. Если у колонки суммы пустое описание типов, в новой строке группы будет Неопределено, и сложение упадёт. Проверка: ТЗ.Колонки.Сумма.ТипЗначения.СодержитТип(Тип("Число")).
  3. Пустые значения в полях группировки. Неопределено и NULL (часто приходит из левого соединения) лучше заменить заранее или в исходном запросе обернуть поле в ЕСТЬNULL.
  4. Дубли ключа для рекурсии. Сравните ТЗ.Количество() с числом строк после Свернуть("Ссылка") у копии таблицы: если строк стало меньше, одна ссылка встречается дважды, и её ветка продублируется.

После построения:

  • ✅ Для рекурсии: Счетчик должен совпасть с ТЗ.Количество(). Разница — это «сироты» (родителя нет в таблице) или строки, замкнутые в кольцо.
  • ✅ Для группировки: сумма по верхнему уровню дерева должна совпасть с ТЗ.Итог("Сумма").

Обработку ошибок держите на уровне вызова, а не внутри цикла по строкам:

Попытка
    Дерево = ДеревоСГруппировкой(ТЗ, "Контрагент, Договор", "Сумма");
Исключение
    ЗаписьЖурналаРегистрации("Построение дерева", УровеньЖурналаРегистрации.Ошибка,,,
        ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    ВызватьИсключение СтрШаблон("Не удалось построить дерево: %1",
        КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;

Попытка внутри цикла на каждую строку маскирует проблему: дерево построится наполовину, а пользователь этого не увидит.

Частые вопросы

Можно ли передать дерево значений параметром в запрос?
Нет. Запрос принимает таблицу значений, массив или список значений. Если данные уже в дереве, сначала соберите нужные строки в таблицу значений.
Как отсортировать готовое дерево вместе с подчинёнными строками?
Методом коллекции строк: Дерево.Строки.Сортировать("Сумма Убыв", Истина). Второй параметр включает сортировку на всех уровнях.
Почему дерево не видно на форме после заполнения?
Дерево в памяти и реквизит формы — разные объекты. Нужен перенос ЗначениеВРеквизитФормы(Дерево, "ИмяРеквизита") в серверной процедуре, а сам реквизит должен иметь тип «Дерево значений» с теми же колонками.
Как получить обратно плоскую таблицу из дерева?
Обойти дерево рекурсивно и для каждой строки без подчинённых вызвать ЗаполнитьЗначенияСвойств(ТЗ.Добавить(), СтрокаДерева). Колонки таблицы создаются по колонкам дерева так же, как в обратную сторону.
Работает ли этот код в тонком клиенте?
Нет, ДеревоЗначений и ТаблицаЗначений недоступны в тонком и веб-клиенте. Построение выносят в процедуру с директивой &НаСервере или в серверный общий модуль.