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

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

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

1. Что такое дерево значений и зачем оно нужно

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

Основные преимущества использования деревьев значений:

  • 📊 Визуализация иерархий: удобно отображать подчиненные элементы (например, подразделения компании или категории товаров).
  • 🔄 Динамическая группировка: возможность программно добавлять/удалять уровни вложенности.
  • 📤 Интеграция с отчетами: многие стандартные отчеты (например, Отчет по продажам) используют деревья для вывода данных.
  • 🔗 Связь с другими объектами: дерево можно преобразовать в XML, JSON или передать в внешнюю систему.

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

Важно понимать, что дерево значений — это не просто "таблица с вложенными строками", а полноценный объект с собственными методами (ПолучитьСтроку(), Добавить(), УстановитьРодителя()), которые позволяют манипулировать данными на уровне кода.

📊 Как часто вы используете деревья значений в 1С?
Ежедневно
Несколько раз в неделю
Редее чем раз в месяц
Никогда не использовал

2. Базовый метод выгрузки: от запроса к дереву

Начнем с простейшего примера: у нас есть запрос, возвращающий список номенклатуры с группировкой по категориям. Задача — преобразовать результат в дерево, где категории будут родительскими узлами, а номенклатура — дочерними.

Синтаксис создания дерева и заполнения его данными из запроса:

Запрос = Новый Запрос;

Запрос.Текст =

"ВЫБРАТЬ

| Номенклатура.Ссылка КАК Номенклатура,

| Номенклатура.Наименование КАК Наименование,

| Номенклатура.Родитель КАК Родитель

|ИЗ

| Справочник.Номенклатура КАК Номенклатура

|УПОРЯДОЧИТЬ ПО

| Наименование";

РезультатЗапроса = Запрос.Выполнить();

Выборка = РезультатЗапроса.Выбрать();

// Создаем дерево значений

Дерево = Новый ДеревоЗначений;

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

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

// Заполняем дерево

Пока Выборка.Следующий() Цикл

Строка = Дерево.Строки.Добавить();

Строка.Номенклатура = Выборка.Номенклатура;

Строка.Наименование = Выборка.Наименование;

// Если есть родитель, устанавливаем связь

Если Не Выборка.Родитель.Пустая() Тогда

РодительскаяСтрока = Дерево.НайтиСтроки(Новый Структура("Номенклатура", Выборка.Родитель))[0];

Строка.Родитель = РодительскаяСтрока;

КонецЕсли;

КонецЦикла;

В этом примере мы:

  1. Выполняем запрос к справочнику Номенклатура.
  2. Создаем дерево с колонками, соответствующими полям запроса.
  3. Для каждой строки результата добавляем строку в дерево.
  4. Если у номенклатуры есть родитель, находим его в дереве и устанавливаем связь через свойство Родитель.
💡

Используйте метод НайтиСтроки() с структурой условий для поиска родительских узлов — это быстрее, чем перебор всех строк в цикле.

Обратите внимание на тип данных колонок: если в запросе поле имеет тип СправочникСсылка.Номенклатура, то и в дереве колонка должна быть объявлена с аналогичным типом. Иначе при попытке присвоения значения возникнет ошибка несоответствия типов.

3. Работа с иерархическими запросами

Часто данные в имеют сложную иерархию, которую нельзя получить одним плоским запросом. Например, у вас есть справочник Контрагенты с подчиненными элементами (филиалы), и нужно выгрузить их в дерево с сохранением структуры. Для этого используются рекурсивные запросы или запросы с обходом иерархии.

Пример запроса с обходом иерархии контрагентов:

Запрос = Новый Запрос;

Запрос.Текст =

"ВЫБРАТЬ

| Контрагенты.Ссылка КАК Контрагент,

| Контрагенты.Наименование КАК Наименование,

| Контрагенты.ЭтотУзел КАК Узел,

| Контрагенты.Уровень КАК Уровень

|ИЗ

| Справочник.Контрагенты КАК Контрагенты

|ГДЕ

| Контрагенты.ПометкаУдаления = ЛОЖЬ

|УПОРЯДОЧИТЬ ПО

| Узел";

РезультатЗапроса = Запрос.Выполнить();

Выборка = РезультатЗапроса.Выбрать();

// Создаем дерево с колонками

Дерево = Новый ДеревоЗначений;

Дерево.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));

Дерево.Колонки.Добавить("Наименование");

Дерево.Колонки.Добавить("Уровень");

// Массив для хранения строк по уровням

Уровни = Новый Массив;

// Заполняем дерево с учетом иерархии

Пока Выборка.Следующий() Цикл

Строка = Дерево.Строки.Добавить();

Строка.Контрагент = Выборка.Контрагент;

Строка.Наименование = Выборка.Наименование;

Строка.Уровень = Выборка.Уровень;

// Если уровень > 1, ищем родителя (предпоследний элемент в пути Узел)

Если Выборка.Уровень > 1 Тогда

ПутьУзла = СтроковыеФункции.Разделить(Выборка.Узел, ".");

РодительскийУзел = ПутьУзла[ПутьУзла.ВГраница() - 1];

РодительскаяСтрока = Уровни[РодительскийУзел];

Строка.Родитель = РодительскаяСтрока;

КонецЕсли;

// Сохраняем строку в массив по ключу Узел

Уровни.Вставить(Выборка.Узел, Строка);

КонецЦикла;

Ключевые моменты этого подхода:

  • 🔍 Используем псевдонимы ЭтотУзел и Уровень, которые автоматически формирует при обходе иерархии.
  • 📌 Узел — это строка вида "1.2.5", где каждая цифра обозначает позицию элемента на своем уровне.
  • 🔄 Для поиска родителя разбиваем Узел по точкам и берем предпоследний элемент.
Что делать если запрос возвращает дублирующиеся Узлы?

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

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

4. Оптимизация производительности

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

Проблема 1: Долгое выполнение цикла заполнения дерева

Если запрос возвращает 50 000 строк, а вы в цикле для каждой строки ищете родителя методом НайтиСтроки(), время выполнения может достичь нескольких минут. Решение — использовать кеширование родительских строк в словаре:

// Создаем словарь для кеширования родителей

Родители = Новый Соответствие;

// Заполняем дерево с кешированием

Пока Выборка.Следующий() Цикл

Строка = Дерево.Строки.Добавить();

// ... заполнение полей строки ...

Если Не Выборка.Родитель.Пустая() Тогда

Если НЕ Родители.СодержитКлюч(Выборка.Родитель.УникальныйИдентификатор()) Тогда

РодительскаяСтрока = Дерево.НайтиСтроки(Новый Структура("Родитель", Выборка.Родитель))[0];

Родители.Вставить(Выборка.Родитель.УникальныйИдентификатор(), РодительскаяСтрока);

КонецЕсли;

Строка.Родитель = Родители.Получить(Выборка.Родитель.УникальныйИдентификатор());

КонецЕсли;

КонецЦикла;

Проблема 2: Избыточные колонки в дереве

Каждая колонка в дереве значений занимает память. Если вам нужны только 3 поля из 20, возвращаемых запросом, создайте дерево только с необходимыми колонками. Это сократит расход памяти на 30-50%.

Проблема 3: Блокировки при работе с транзакциями

Если выгрузка данных происходит в рамках транзакции, длительные операции могут блокировать другие сеансы. Решение — разбивать большие выборки на пакеты (по 1000-5000 строк) и обрабатывать их отдельно:

РазмерПакета = 5000;

НомерПакета = 0;

Пока Истина Цикл

Запрос.УстановитьПараметр("Смещение", НомерПакета * РазмерПакета);

Результат = Запрос.Выполнить();

Выборка = Результат.Выбрать();

Если НЕ Выборка.Следующий() Тогда

Прервать;

КонецЕсли;

// Обрабатываем пакет

Пока Выборка.Следующий() Цикл

// ... обработка строки ...

КонецЦикла;

НомерПакета = НомерПакета + 1;

КонецЦикла;

Для крайне больших объемов данных (более 100 000 строк) рассмотрите возможность использования внешних обработок или выгрузки в промежуточный файл (JSON, XML), а не в дерево значений.

💡

Оптимизация работы с деревьями значений сводится к трем принципам: минимизация колонок, кеширование родительских узлов и пакетная обработка больших выборок.

5. Типичные ошибки и их решения

Даже опытные разработчики сталкиваются с ошибками при работе с деревьями значений. Рассмотрим наиболее распространенные случаи и способы их исправления.

Ошибка 1: "Недопустимое значение свойства 'Родитель'"

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

Если ТипЗнч(РодительскаяСтрока) = Тип("СтрокаДереваЗначений") И

РодительскаяСтрока.Дерево = Дерево Тогда

Строка.Родитель = РодительскаяСтрока;

Иначе

Сообщить("Ошибка: неверный родитель для строки " + Строка.Наименование);

КонецЕсли;

Ошибка 2: "Колонка не найдена"

Происходит, если в коде обращаетесь к колонке, которая не была объявлена в дереве. Всегда проверяйте существование колонки перед обращением:

Если Дерево.Колонки.Найти("Наименование") = Неопределено Тогда

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

КонецЕсли;

Ошибка 3: Зацикливание при рекурсивном обходе

Если в данных есть циклические ссылки (например, справочник ссылается сам на себя), при построении дерева может возникнуть бесконечный цикл. Решение — отслеживать посещенные узлы:

ПосещенныеСсылки = Новый Массив;

Процедура ДобавитьУзел(Узел, РодительскаяСтрока = Неопределено)

Если ПосещенныеСсылки.Найти(Узел.УникальныйИдентификатор()) <> Неопределено Тогда

Возврат; // Узел уже обработан

КонецЕсли;

ПосещенныеСсылки.Добавить(Узел.УникальныйИдентификатор());

Строка = Дерево.Строки.Добавить();

// ... заполнение строки ...

Если РодительскаяСтрока <> Неопределено Тогда

Строка.Родитель = РодительскаяСтрока;

КонецЕсли;

// Рекурсивный обход дочерних элементов

Для Каждого ДочернийУзел Из Узел.ДочерниеЭлементы Цикл

ДобавитьУзел(ДочернийУзел, Строка);

КонецЦикла;

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

Другие распространенные ошибки:

Ошибка Причина Решение
"Индекс вне границ" Обращение к несуществующей строке по индексу Проверяйте границы массива строк перед обращением
"Тип не совпадает" Попытка присвоить значение неверного типа колонке Соблюдайте соответствие типов при объявлении колонок
"Дерево заблокировано" Попытка изменить дерево во время итерации по его строкам Используйте копию строк для модификации
"Слишком много строк" Превышен лимит строк (зависит от версии платформы) Разбивайте данные на несколько деревьев или используйте пагинацию

✅ Убедитесь, что все колонки дерева объявлены с правильными типами

✅ Проверьте отсутствие циклических ссылок в исходных данных

✅ Оцените объем данных — при >10 000 строк используйте пакетную обработку

✅ Кешируйте родительские строки для ускорения поиска-->

6. Практический пример: выгрузка документов с группировкой по контрагентам

Рассмотрим реальный кейс: нужно выгрузить все документы РеализацияТоваровУслуг за месяц, сгруппировав их по контрагентам. При этом у некоторых контрагентов есть подчиненные филиалы, которые тоже должны отображаться в иерархии.

Шаг 1: Формируем запрос с обходом иерархии контрагентов и связью с документами:

Запрос = Новый Запрос;

Запрос.Текст =

"ВЫБРАТЬ

| Контрагенты.Ссылка КАК Контрагент,

| Контрагенты.Наименование КАК НаименованиеКонтрагента,

| Контрагенты.ЭтотУзел КАК УзелКонтрагента,

| Контрагенты.Уровень КАК УровеньКонтрагента,

| Документы.Ссылка КАК Документ,

| Документы.Дата КАК Дата,

| Документы.СуммаДокумента КАК Сумма

|ИЗ

| Справочник.Контрагенты КАК Контрагенты

| ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Документы

| ПО Контрагенты.Ссылка = Документы.Контрагент

|ГДЕ

| Документы.Дата МЕЖДУ &НачалоПериода И &КонецПериода

|УПОРЯДОЧИТЬ ПО

| УзелКонтрагента,

| Дата";

Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ТекущаяДата()));

Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ТекущаяДата()));

Шаг 2: Создаем дерево с необходимыми колонками:

Дерево = Новый ДеревоЗначений;

Дерево.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));

Дерево.Колонки.Добавить("НаименованиеКонтрагента");

Дерево.Колонки.Добавить("Документ", Новый ОписаниеТипов("ДокументСсылка.РеализацияТоваровУслуг"));

Дерево.Колонки.Добавить("Дата");

Дерево.Колонки.Добавить("Сумма");

Шаг 3: Заполняем дерево с учетом иерархии контрагентов и привязки документов:

Результат = Запрос.Выполнить();

Выборка = Результат.Выбрать();

// Кеш для контрагентов

КонтрагентыКеш = Новый Соответствие;

// Текущий контрагент для группировки документов

ТекущийКонтрагент = Неопределено;

ТекущаяСтрокаКонтрагента = Неопределено;

Пока Выборка.Следующий() Цикл

// Если контрагент изменился, добавляем его в дерево

Если ТекущийКонтрагент <> Выборка.Контрагент Тогда

ТекущийКонтрагент = Выборка.Контрагент;

// Ищем родительскую строку (если есть родитель в иерархии)

РодительскаяСтрока = Неопределено;

Если Выборка.УровеньКонтрагента > 1 Тогда

Путь = СтроковыеФункции.Разделить(Выборка.УзелКонтрагента, ".");

РодительскийУзел = Путь[Путь.ВГраница() - 1];

Если КонтрагентыКеш.СодержитКлюч(РодительскийУзел) Тогда

РодительскаяСтрока = КонтрагентыКеш[РодительскийУзел];

КонецЕсли;

КонецЕсли;

// Добавляем строку контрагента

ТекущаяСтрокаКонтрагента = Дерево.Строки.Добавить();

ТекущаяСтрокаКонтрагента.Контрагент = Выборка.Контрагент;

ТекущаяСтрокаКонтрагента.НаименованиеКонтрагента = Выборка.НаименованиеКонтрагента;

Если РодительскаяСтрока <> Неопределено Тогда

ТекущаяСтрокаКонтрагента.Родитель = РодительскаяСтрока;

КонецЕсли;

// Сохраняем строку в кеш

КонтрагентыКеш.Вставить(Выборка.УзелКонтрагента, ТекущаяСтрокаКонтрагента);

КонецЕсли;

// Добавляем документ как дочернюю строку

Если Выборка.Документ <> Неопределено Тогда

СтрокаДокумента = Дерево.Строки.Добавить();

СтрокаДокумента.Документ = Выборка.Документ;

СтрокаДокумента.Дата = Выборка.Дата;

СтрокаДокумента.Сумма = Выборка.Сумма;

СтрокаДокумента.Родитель = ТекущаяСтрокаКонтрагента;

КонецЕсли;

КонецЦикла;

В результате мы получим дерево, где:

  • 📁 Корневые узлы — контрагенты верхнего уровня.
  • 📂 Дочерние узлы — филиалы контрагентов.
  • 📄 Листья — документы реализации, привязанные к соответствующим контрагентам.
💡

При выгрузке данных с группировкой всегда кешируйте родительские узлы — это ускорит процесс в 5-10 раз по сравнению с поиском строки при каждой итерации.

7. Альтернативные подходы: когда дерево значений не подходит

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

1. Таблица значений с колонкой "Уровень"

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

Таблица = Новый ТаблицаЗначений;

Таблица.Колонки.Добавить("Элемент");

Таблица.Колонки.Добавить("Наименование");

Таблица.Колонки.Добавить("Уровень");

// Заполнение таблицы с указанием уровня

Пока Выборка.Следующий() Цикл

Строка = Таблица.Добавить();

Строка.Элемент = Выборка.Элемент;

Строка.Наименование = Выборка.Наименование;

Строка.Уровень = Выборка.Уровень;

КонецЦикла;

2. JSON-структура для интеграции

Если данные нужно передать во внешнюю систему, удобнее сформировать JSON:

JSONДерево = Новый Структура();

JSONДерево.Вставить("name", "Корневой элемент");

JSONДерево.Вставить("children", Новый Массив());

// Рекурсивное заполнение JSON

Процедура ДобавитьВJSON(Узел, РодительJSON)

ЭлементJSON = Новый Структура();

ЭлементJSON.Вставить("name", Узел.Наименование);

ЭлементJSON.Вставить("id", Узел.УникальныйИдентификатор());

Если РодительJSON.СодержитКлюч("children") Тогда

РодительJSON.children.Добавить(ЭлементJSON);

Иначе

РодительJSON.Вставить("children", Новый Массив(ЭлементJSON));

КонецЕсли;

Для Каждого ДочернийУзел Из Узел.ДочерниеЭлементы Цикл

ДобавитьВJSON(ДочернийУзел, ЭлементJSON);

КонецЦикла;

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

3. Временные таблицы для сложных отчетов

Если дерево нужно только для формирования отчета, рассмотрите возможность использования временных таблиц в запросе:

Запрос.Текст =

"ВЫБРАТЬ

| Родитель.Наименование КАК Родитель,

| Дочерний.Наименование КАК Дочерний

|ПОМЕСТИТЬ ВТ_Иерархия

|ИЗ

| Справочник.Элементы КАК Родитель

| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Элементы КАК Дочерний

| ПО Дочерний.Родитель = Родитель.Ссылка

|ИНДЕКСИРОВАТЬ ПО

| Родитель

|;

|

|////////////////////////////////////////////

|ВЫБРАТЬ

| ВТ_Иерархия.Родитель,

| ВТ_Иерархия.Дочерний

|ИЗ

| ВТ_Иерархия КАК ВТ_Иерархия";

Выбор подхода зависит от задачи:

  • 🌳 Дерево значений: интерактивная работа в форме, визуализация иерархии.
  • 📊 Таблица значений: простая группировка без вложенности.
  • 🌐 JSON/XML: интеграция с внешними системами.
  • 📈 Временные таблицы: сложные отчеты с многоуровневой аналитикой.
💡

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

8. Интеграция с внешними системами

Часто данные из дерева значений нужно передать во внешнюю систему (например, в Excel, API или базу данных). Рассмотрим основные сценарии:

1. Выгрузка в Excel

Используйте объект ExcelДокумент для сохранения дерева в формате .xlsx:

Excel = Новый ExcelДокумент;

Лист = Excel.Листы.Добавить("Данные");

// Заголовки колонок

Для Каждого Колонка Из Дерево.Колонки Цикл

Лист.Ячейка(1, Колонка.Индекс + 1).Значение = Колонка.Имя;

КонецЦикла;

// Данные с учетом иерархии

Процедура ЗаписатьСтроки(Строк