В процессе разработки на платформе 1С:Предприятие программисты часто сталкиваются с удобной особенностью: при выборе табличной части объекта в запросе она возвращается как вложенный результат запроса. Это позволяет обрабатывать данные иерархически, сохраняя контекст основной записи. Однако возникает закономерный вопрос: можно ли создать «синтетическую» вложенную таблицу, данные для которой берутся не из встроенной табличной части, а, например, из регистра сведений или другого справочника, не размножая при этом строки основного результата? Разберем, почему стандартный язык запросов 1С накладывает здесь ограничения и какими способами мы можем обойти эту проблему.
Для начала проанализируем, как работает механизм вложенных таблиц для табличных частей. Когда мы пишем в запросе ВЫБРАТЬ Ссылка, Состав.(Номенклатура, Количество) ИЗ Документ.РеализацияТоваровУслуг, платформа 1С на уровне СУБД выполняет оптимизированную выборку. Поле Состав в данном случае является специальным типом данных — РезультатЗапроса.
К сожалению, синтаксис языка запросов 1С не поддерживает коррелированные подзапросы внутри блока ВЫБРАТЬ для произвольных таблиц. Мы не можем написать конструкцию вида ВЫБРАТЬ Ссылка, (ВЫБРАТЬ Поле ИЗ Регистр ГДЕ Регистр.Объект = Ссылка) КАК МояТаблица. Это ограничение платформы. Реализовать «синтетическое» поле-результат запроса напрямую невозможно. Однако существует несколько эффективных обходных путей, которые мы сейчас подробно рассмотрим.
Самый близкий к «нативному» поведению способ — это использование предложения ИТОГИ. Этот метод позволяет сгруппировать данные таким образом, чтобы при обходе выборки мы могли получать детальные записи как подчиненную выборку. Посмотрим на пример реализации:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Спр.Ссылка КАК ОсновнаяСсылка,
| Рег.Период КАК ПериодЗаписи,
| Рег.Значение КАК ЗначениеИзРегистра
|ИЗ
| Справочник.Номенклатура КАК Спр
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Рег
| ПО Спр.Ссылка = Рег.Номенклатура
|ИТОГИ ПО
| ОсновнаяСсылка";
РезультатЗапроса = Запрос.Выполнить();
// Выборка по верхнему уровню (аналог строк справочника)
ВыборкаВерхнийУровень = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаВерхнийУровень.Следующий() Цикл
Сообщить("Объект: " + ВыборкаВерхнийУровень.ОсновнаяСсылка);
// Получаем "вложенную таблицу" через детальные записи
ВыборкаДетали = ВыборкаВерхнийУровень.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
// Здесь обрабатываем данные из регистра, как если бы это была ТЧ
Сообщить("--- Запись регистра: " + ВыборкаДетали.ЗначениеИзРегистра);
КонецЦикла;
КонецЦикла;
Выясним причину, почему этот метод удобен: он позволяет избежать дублирования логики обработки основного объекта, так как ВыборкаВерхнийУровень содержит только уникальные ссылки справочника. При этом данные из регистра доступны «внутри» каждой итерации.
Если ваша задача заключается в выводе данных в отчет или программном получении сложной структуры, СКД — самый мощный инструмент. В СКД механизм связи наборов данных работает именно так, как хотел автор вопроса: для каждой строки первого набора выполняется «подзапрос» или соединение со вторым набором. Разберем по шагам, как это настроить программно:
Ссылка (Источник) и Номенклатура (Приемник).Ссылке, а внутри неё — Детальные записи.Программно результат СКД можно выгрузить в ДеревоЗначений. В этом случае у каждой строки дерева (справочника) будет коллекция подчиненных строк (регистра), что практически идентично работе с табличными частями.
В высоконагруженных системах использование ИТОГИ или вложенных таблиц ТЧ в одном запросе может привести к избыточному потреблению памяти или неоптимальному плану SQL. Проанализируем ситуацию, когда данных очень много. В этом случае лучше использовать пакетный запрос и программную «сшивку».
Рассмотрим пример эффективного алгоритма:
МенеджерВТ = Новый МенеджерВременных Таблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Помещаем ссылки во временную таблицу для индексации
Запрос.Текст =
"ВЫБРАТЬ Ссылка ПОМЕСТИТЬ ВТ_Ссылки ИЗ Справочник.Номенклатура ГДЕ ... ;
|ВЫБРАТЬ Ссылка ИЗ ВТ_Ссылки;
|ВЫБРАТЬ Рег.* ИЗ РегистрСведений.Цены КАК Рег
|ГДЕ Рег.Номенклатура В (ВЫБРАТЬ Ссылка ИЗ ВТ_Ссылки)";
Результаты = Запрос.ВыполнитьПакет();
ТаблицаОбъектов = Результаты[1].Выгрузить();
ТаблицаДанныхРегистра = Результаты[2].Выгрузить();
// Индексируем для быстрого поиска
ТаблицаДанныхРегистра.Индексы.Добавить("Номенклатура");
Для Каждого СтрокаОбъекта Из ТаблицаОбъектов Цикл
// Имитируем вложенную таблицу через НайтиСтроки
ПараметрыОтбора = Новый Структура("Номенклатура", СтрокаОбъекта.Ссылка);
СтрокиРегистра = ТаблицаДанныхРегистра.НайтиСтроки(ПараметрыОтбора);
// СтрокиРегистра теперь — массив, содержащий аналог вашей вложенной ТЧ
КонецЦикла;
Этот метод является наиболее производительным для больших объемов данных. Мы избегаем проблемы N+1 (когда на каждую строку основного запроса выполняется отдельный запрос к БД), так как получаем все данные всего за два обращения к СУБД.
Разберем технический аспект: табличные части хранятся в отдельных физических таблицах СУБД, но на уровне метаданных они жестко подчинены владельцу. Платформа при генерации SQL-запроса «знает», как эффективно соединить эти таблицы. Для регистров же связь может быть произвольной, поэтому универсального механизма «вложенного поля» для них не предусмотрено. Это сделано для защиты разработчика от написания заведомо медленных запросов, которые могли бы «подвесить» сервер при попытке сформировать вложенный результат для миллионов записей регистра.
Подводя итог, можно сказать: хотя прямого синтаксиса «выбрать запрос как поле» не существует, 1С предоставляет достаточно инструментов для достижения того же результата. Мы рекомендуем использовать ИТОГИ для простых задач обработки, СКД для построения сложных отчетов и деревьев, и метод «двух запросов» для оптимизации производительности в сложных алгоритмах. Понимание этих механизмов позволяет писать чистый и быстрый код, сохраняя удобную логику обработки вложенных данных.