Работа с данными в 1С:Предприятие часто требует преобразования результатов запросов в удобные структуры для дальнейшей обработки. Одним из самых востребованных форматов является дерево значений — гибкий инструмент, позволяющий хранить иерархические данные, группировать записи и динамически формировать отчеты. Однако многие разработчики сталкиваются с трудностями при попытке перенести плоскую таблицу запроса в древовидную структуру, особенно когда речь идет о сложных связях между данными или больших объемах информации.
В этой статье мы разберем не только базовые методы выгрузки, но и нюансы работы с иерархическими запросами, оптимизацией производительности при обработке тысяч строк, а также типичные ошибки, которые приводят к падению системы или некорректному отображению данных. Особое внимание уделим различиям между платформами 1С 8.2 и 8.3, так как синтаксис и доступные методы могут существенно отличаться.
Если вы только начинаете осваивать язык запросов 1С или уже имеете опыт, но хотите систематизировать знания — этот материал поможет закрыть пробелы. Мы не будем ограничиваться стандартными примерами из документации, а рассмотрим реальные кейсы, включая работу с рекурсивными связями, динамическим формированием колонок и интеграцией с внешними системами через HTTP-Сервисы.
1. Что такое дерево значений и зачем оно нужно
Дерево значений в 1С — это объект встроенного языка, представляющий собой иерархическую структуру данных, где каждая строка может содержать дочерние строки. В отличие от обычной таблицы значений, дерево позволяет организовывать данные в виде родительско-дочерних связей, что критично для отображения иерархий (например, номенклатурных групп, организационной структуры или многоуровневых справочников).
Основные преимущества использования деревьев значений:
- 📊 Визуализация иерархий: удобно отображать подчиненные элементы (например, подразделения компании или категории товаров).
- 🔄 Динамическая группировка: возможность программно добавлять/удалять уровни вложенности.
- 📤 Интеграция с отчетами: многие стандартные отчеты 1С (например,
Отчет по продажам) используют деревья для вывода данных. - 🔗 Связь с другими объектами: дерево можно преобразовать в
XML,JSONили передать в внешнюю систему.
Типичный сценарий применения — выгрузка данных из запроса, где результатом является плоская таблица, но логика бизнес-процесса требует иерархического представления. Например, запрос возвращает список документов с указанием контрагентов, а вам нужно сгруппировать документы по контрагентам, причем у некоторых контрагентов есть подчиненные структуры (филиалы).
Важно понимать, что дерево значений — это не просто "таблица с вложенными строками", а полноценный объект с собственными методами (ПолучитьСтроку(), Добавить(), УстановитьРодителя()), которые позволяют манипулировать данными на уровне кода.
2. Базовый метод выгрузки: от запроса к дереву
Начнем с простейшего примера: у нас есть запрос, возвращающий список номенклатуры с группировкой по категориям. Задача — преобразовать результат в дерево, где категории будут родительскими узлами, а номенклатура — дочерними.
Синтаксис создания дерева и заполнения его данными из запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.Родитель КАК Родитель
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|УПОРЯДОЧИТЬ ПО
| Наименование";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Создаем дерево значений
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
// Заполняем дерево
Пока Выборка.Следующий() Цикл
Строка = Дерево.Строки.Добавить();
Строка.Номенклатура = Выборка.Номенклатура;
Строка.Наименование = Выборка.Наименование;
// Если есть родитель, устанавливаем связь
Если Не Выборка.Родитель.Пустая() Тогда
РодительскаяСтрока = Дерево.НайтиСтроки(Новый Структура("Номенклатура", Выборка.Родитель))[0];
Строка.Родитель = РодительскаяСтрока;
КонецЕсли;
КонецЦикла;
В этом примере мы:
- Выполняем запрос к справочнику
Номенклатура. - Создаем дерево с колонками, соответствующими полям запроса.
- Для каждой строки результата добавляем строку в дерево.
- Если у номенклатуры есть родитель, находим его в дереве и устанавливаем связь через свойство
Родитель.
Используйте метод НайтиСтроки() с структурой условий для поиска родительских узлов — это быстрее, чем перебор всех строк в цикле.
Обратите внимание на тип данных колонок: если в запросе поле имеет тип СправочникСсылка.Номенклатура, то и в дереве колонка должна быть объявлена с аналогичным типом. Иначе при попытке присвоения значения возникнет ошибка несоответствия типов.
3. Работа с иерархическими запросами
Часто данные в 1С имеют сложную иерархию, которую нельзя получить одним плоским запросом. Например, у вас есть справочник Контрагенты с подчиненными элементами (филиалы), и нужно выгрузить их в дерево с сохранением структуры. Для этого используются рекурсивные запросы или запросы с обходом иерархии.
Пример запроса с обходом иерархии контрагентов:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Контрагент,
| Контрагенты.Наименование КАК Наименование,
| Контрагенты.ЭтотУзел КАК Узел,
| Контрагенты.Уровень КАК Уровень
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.ПометкаУдаления = ЛОЖЬ
|УПОРЯДОЧИТЬ ПО
| Узел";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Создаем дерево с колонками
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
Дерево.Колонки.Добавить("Наименование");
Дерево.Колонки.Добавить("Уровень");
// Массив для хранения строк по уровням
Уровни = Новый Массив;
// Заполняем дерево с учетом иерархии
Пока Выборка.Следующий() Цикл
Строка = Дерево.Строки.Добавить();
Строка.Контрагент = Выборка.Контрагент;
Строка.Наименование = Выборка.Наименование;
Строка.Уровень = Выборка.Уровень;
// Если уровень > 1, ищем родителя (предпоследний элемент в пути Узел)
Если Выборка.Уровень > 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).Значение = Колонка.Имя;
КонецЦикла;
// Данные с учетом иерархии
Процедура ЗаписатьСтроки(Строк