Как собрать дерево значений из таблицы значений в 1С 8.3

· 9 мин

Содержание

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

Готового глобального метода вроде ЗагрузитьДерево() в платформе нет — дерево строится либо через запрос, либо кодом. Ниже — три рабочих приёма с полными примерами на встроенном языке 1С, разбор, когда какой брать, и что проверить, если дерево собралось неправильно.

Когда плоскую таблицу переводят в дерево

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

Переводят таблицу в дерево в нескольких ситуациях:

  • 📊 Вывод на форму с разворачиванием уровней. Элемент формы «дерево» умеет сворачивать и разворачивать ветви — табличное поле так не умеет.
  • 🧾 Группировка с подытогами. Суммы по контрагенту, внутри — по договорам, внутри — по документам.
  • 🗂 Отражение иерархии справочника (группы и элементы, подразделения, статьи затрат).
  • ⚙️ Промежуточная обработка перед выгрузкой в табличный документ или перед обменом.

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

Что должно быть в исходной таблице

Прежде чем собирать дерево, у таблицы значений должны быть заданы два момента.

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

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

Признак иерархии. Платформа сама не знает, кто чей родитель. Нужна одна из двух схем:

  • пара колонок Код / Родитель, где Родитель хранит код владельца (у корневых строк — Неопределено или ноль);
  • набор колонок-группировок (Контрагент, Договор, Сумма), по которым строят уровни запросом.

От того, какая схема у вас, и зависит выбор способа.

Способ 1. Выгрузка результата запроса по группировкам

Самый короткий путь, если уровни дерева — это группировки. Таблицу значений кладут в запрос параметром, добавляют секцию ИТОГИ ... ПО, а результат выгружают деревом.

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

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

На выходе Дерево — объект ДеревоЗначений с колонками Контрагент, Договор, Сумма. Верхний уровень — строки по контрагентам, внутри — по договорам, в колонке Сумма на каждом уровне стоит подытог.

Чтобы в дереве остались не только группировки, но и детальные записи, используйте обход ОбходРезультатаЗапроса.ПоГруппировкамСИерархией вместе с полем-признаком ТипЗаписи, либо собирайте детальный уровень отдельным запросом. Чистый ПоГруппировкам отдаёт только уровни итогов.

Когда группировка идёт по иерархическому справочнику (например, по группам номенклатуры), добавьте слово ИЕРАРХИЯ:

|ИТОГИ
|	СУММА(Сумма)
|ПО
|	Номенклатура ИЕРАРХИЯ

Тогда дерево повторит структуру групп справочника, а не просто перечень встретившихся значений.

Способ хорош скоростью написания и тем, что подытоги считаются платформой. Минус — он навязывает структуру «группировка за группировкой»: произвольную связь «код → родитель» так не разложить.

Способ 2. Рекурсивная сборка по полю родителя

Когда в таблице уже есть колонки Код и Родитель, дерево собирают кодом. Идея: берём строки, у которых родитель совпадает с текущим узлом, добавляем их в дерево и для каждой повторяем то же самое для её потомков.

Функция ПостроитьДерево(ТЗ) Экспорт

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

	ДобавитьПотомков(ТЗ, Дерево.Строки, Неопределено);

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

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

Процедура ДобавитьПотомков(ТЗ, СтрокиРодителя, КодРодителя)

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

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

Что здесь важно:

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

НайтиСтроки каждый раз перебирает всю таблицу. На тысяче строк это незаметно, на десятках тысяч — заметно тормозит: получается обход порядка квадрата числа строк. Для больших объёмов сразу смотрите следующий раздел про индекс.

Плюс способа — он разложит любую связь «ребёнок → родитель», даже если уровни неоднородные. Минус — рекурсия и ручная работа с колонками.

Способ 3. Через менеджер временных таблиц

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

МВТ = Новый МенеджерВременныхТаблиц;

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

// Временная таблица ВТ_Исходная теперь доступна в следующих запросах МВТ
Запрос.Текст =
"ВЫБРАТЬ
|	ВТ_Исходная.Подразделение КАК Подразделение,
|	ВТ_Исходная.Сотрудник КАК Сотрудник,
|	ВТ_Исходная.Сумма КАК Сумма
|ИЗ
|	ВТ_Исходная КАК ВТ_Исходная
|ИТОГИ
|	СУММА(Сумма)
|ПО
|	Подразделение";
Результат = Запрос.Выполнить();

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

Выгрузка результата — та же, что в способе 1. Разница в том, что исходная таблица лежит во временной таблице менеджера: к ней можно присоединять справочники, регистры, другие временные таблицы и только потом сворачивать в дерево. Это рабочий вариант, когда дерево — финальный шаг сложной обработки, а не самостоятельная задача.

Чтобы не путаться, какой способ брать под конкретную задачу:

СпособКогда братьЧто нужно в таблице
Выгрузка запроса по группировкамУровни — это группировки с подытогами (контрагент → договор)Заданные типы колонок
Рекурсия по родителюПроизвольная связь «ребёнок → родитель», неоднородные уровниКолонки Код и Родитель
Менеджер временных таблицДерево — финал сложной обработки с соединениямиЗаданные типы колонок

Для большинства отчётов хватает первого способа. Рекурсию берут там, где иерархия хранится явной парой «код / родитель», а менеджер временных таблиц — когда таблицу всё равно приходится соединять с регистрами и справочниками.

Перед сборкой дерева проверьте
Выполнено: 0 / 5

Как ускорить сборку на больших таблицах

Рекурсия из способа 2 замедляется из-за того, что НайтиСтроки каждый раз сканирует таблицу целиком. Решение — один раз построить индекс «родитель → список его детей» в Соответствие, а дальше брать детей из него за один шаг.

Функция ПостроитьДеревоБыстро(ТЗ) Экспорт

	// Индекс: код родителя -> массив строк-детей
	Индекс = Новый Соответствие;
	Для Каждого Строка Из ТЗ Цикл
		Список = Индекс[Строка.Родитель];
		Если Список = Неопределено Тогда
			Список = Новый Массив;
			Индекс.Вставить(Строка.Родитель, Список);
		КонецЕсли;
		Список.Добавить(Строка);
	КонецЦикла;

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

	РазложитьУровень(Индекс, Дерево.Строки, Неопределено);
	Возврат Дерево;

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

Процедура РазложитьУровень(Индекс, СтрокиРодителя, КодРодителя)

	Дети = Индекс[КодРодителя];
	Если Дети = Неопределено Тогда
		Возврат;
	КонецЕсли;

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

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

Теперь каждая строка обрабатывается один раз, а поиск детей — мгновенный. Разница на таблице в 50–100 тысяч строк — секунды против минут.

Ещё пара приёмов для тяжёлых случаев:

  • 🧮 Считайте дерево на сервере. Сборка в контексте клиента гоняет данные туда-обратно. Выносите функцию в общий модуль с признаком «Сервер».
  • 🧱 Не стройте дерево в цикле проведения или в цикле по документам. Если дерево нужно для отчёта — собирайте его один раз из готового запроса, а не по одной ветке на каждой итерации.
  • ✂️ Отсекайте лишние колонки до сборки: чем уже таблица, тем меньше копирует ЗаполнитьЗначенияСвойств.

Почему дерево собирается неправильно

Самые частые причины, по которым результат не совпадает с ожиданием:

  • Строки дерева пустые. Колонки дерева названы не так, как в таблице, — ЗаполнитьЗначенияСвойств сопоставляет поля по имени и молча пропускает несовпадающие. Сверьте имена колонок.
  • Все строки встали в один уровень. В рекурсии корни ищутся по Неопределено, а в таблице родитель корневых строк равен нулю (или наоборот). Приведите признак корня к одному виду.
  • Зависание или переполнение стека. В данных есть кольцо: строка А ссылается на Б, Б — обратно на А, либо строка указывает родителем саму себя. Рекурсия не остановится. Перед сборкой отбросьте строки, где Код = Родитель, и при обходе ведите список уже добавленных кодов.
  • Пропали детальные записи. Ждали листья, а Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам) вернул только уровни группировок. Нужен ПоГруппировкамСИерархией или отдельный запрос за детальными строками.
  • Ошибка про тип поля при ПОМЕСТИТЬ. У колонки таблицы не задан тип. Пересоздайте колонку через Новый ОписаниеТипов(...).

Защита от колец для рекурсивного способа — соответствие уже обработанных кодов:

Если Обработанные[СтрокаТЗ.Код] <> Неопределено Тогда
	Продолжить; // этот код уже в дереве — пропускаем
КонецЕсли;
Обработанные.Вставить(СтрокаТЗ.Код, Истина);

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

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