Как правильно обратиться к реквизитам табличной части в запросе 1С и почему не работают вложенные выборки?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

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

Разбор исходной проблемы: почему не работал старый код

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


Запрос = Новый запрос;
Запрос.Текст = "ВЫБРАТЬ
               |    Абоненты.ТипТелефона КАК ТипТелефона,
               |    Абоненты.Данные.(ВидЛьготы КАК ВидЛьготы) КАК Данные
               |ИЗ
               |    Справочник.Абоненты КАК Абоненты
               |ГДЕ
               |    Абоненты.Ссылка = &Абонент";

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


Выгрузка = Результат.Выгрузить();
Для каждого Строка из Выгрузка Цикл
    //Объект.ВидЛьготы = Строка.Данные; // Здесь происходила ошибка!
    Объект.ТипТелефона = Строка.ТипТелефона;
КонецЦикла;

Выясним причину этой ошибки. Когда мы выбираем табличную часть (в данном случае Данные справочника Абоненты) внутри круглых скобок в основном списке полей, платформа 1С возвращает не простое значение и не ссылку, а целую вложенную таблицу. Тип данных в колонке Данные будет напрямую зависеть от того, каким методом мы обрабатываем результат запроса:

  1. Если мы выгружаем результат запроса в таблицу значений с помощью метода Результат.Выгрузить(), то в колонке Данные для каждой строки будет находиться полноценный объект типа ТаблицаЗначений.
  2. Если мы осуществляем последовательный обход выборки с помощью метода Результат.Выбрать(), то в поле Данные будет находиться объект типа РезультатЗапроса, к которому нужно применять дополнительный метод Выбрать().

Попытка напрямую присвоить Объект.ВидЛьготы = Строка.Данные приводит к ошибке несоответствия типов, поскольку мы пытаемся записать целую таблицу в одно простое поле (реквизит) объекта.

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

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

Вариант А. Через выгрузку в Таблицу значений (метод Выгрузить())

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


Выгрузка = Результат.Выгрузить();
Для каждого Строка из Выгрузка Цикл
    Объект.ТипТелефона = Строка.ТипТелефона;
    
    // Получаем вложенную ТаблицуЗначений из ячейки текущей строки
    ТаблицаДанных = Строка.Данные; 
    Для каждого СтрокаДанные из ТаблицаДанных Цикл
        // Обращаемся к реквизиту табличной части внутри вложенного цикла
        Объект.ВидЛьготы = СтрокаДанные.ВидЛьготы; 
    КонецЦикла;
КонецЦикла;

Вариант Б. Через последовательный обход выборки (метод Выбрать())

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


Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
    Объект.ТипТелефона = Выборка.ТипТелефона;
    
    // Получаем подчиненную выборку из поля РезультатаЗапроса
    ВыборкаДанные = Выборка.Данные.Выбрать();
    Пока ВыборкаДанные.Следующий() Цикл
        Объект.ВидЛьготы = ВыборкаДанные.ВидЛьготы;
    КонецЦикла;
КонецЦикла;

Почему использование вложенных таблиц в запросах — плохая практика?

Как справедливо заметили участники форума, за конструкцию вида Абоненты.Данные.(ВидЛьготы КАК ВидЛьготы) в профессиональной разработке 1С давно принято критиковать. Данный синтаксис считается устаревшим (legacy-кодом) по ряду веских причин:

  1. Проблемы с производительностью СУБД: При трансляции такого запроса на уровень SQL-сервера платформа 1С вынуждена генерировать сложные коррелированные подзапросы или избыточные левые соединения. СУБД (будь то MS SQL Server, PostgreSQL или IBM DB2) крайне неэффективно строят планы выполнения для подобных конструкций, что приводит к долгому выполнению запроса, избыточному чтению страниц данных и блокировкам таблиц в многопользовательском режиме.
  2. Нерациональное использование оперативной памяти: Создание иерархических структур данных (таблиц внутри таблиц) на сервере приложений 1С создает колоссальную нагрузку на оперативную память и сборщик мусора, особенно при работе с большими объемами информации.
  3. Нарушение стандартов разработки: Официальные стандарты разработки фирмы 1С прямо предписывают отказываться от использования вложенных выборок табличных частей в пользу прямых запросов к физическим таблицам СУБД.
  4. Сложность поддержки: Чтение, отладка и доработка кода со вложенными циклами и выборками существенно усложняют работу других программистов, которые будут сопровождать вашу конфигурацию.

Правильное современное решение: запрос напрямую к табличной части

Проанализируем правильный подход, предложенный в Сообщении 4. Вместо того чтобы выбирать данные из основной таблицы справочника и пытаться «вытащить» из нее табличную часть вложенным полем, мы должны написать запрос напрямую к табличной части как к самостоятельной физической таблице.

Для этого в качестве источника данных в секции ИЗ мы указываем Справочник.Абоненты.Данные. Все реквизиты «шапки» (самого справочника-владельца) мы легко можем получить через точку от поля Ссылка.

Рассмотрим правильный текст запроса и его последующую обработку в коде:


Запрос = Новый запрос;
Запрос.Текст = "ВЫБРАТЬ 
               |    АбонентыДанные.Ссылка.ТипТелефона КАК ТипТелефона,
               |    АбонентыДанные.ВидЛьготы КАК ВидЛьготы
               |ИЗ
               |    Справочник.Абоненты.Данные КАК АбонентыДанные
               |ГДЕ 
               |    АбонентыДанные.Ссылка = &Абонент";

Запрос.УстановитьПараметр("Абонент", Объект.Абонент);
Результат = Запрос.Выполнить();

// Теперь мы получаем плоскую таблицу, которую очень легко обойти в один цикл
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
    Объект.ТипТелефона = Выборка.ТипТелефона;
    Объект.ВидЛьготы   = Выборка.ВидЛьготы;
КонецЦикла;

Посмотрим на преимущества этого решения:

Другие альтернативные подходы для сложных задач

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

Левое соединение (LEFT JOIN)

Если вам необходимо гарантированно получить данные «шапки» объекта (например, справочника или документа) даже в том случае, если его табличная часть абсолютно пуста, запроса напрямую к ТЧ будет недостаточно (так как пустая ТЧ не вернет ни одной строки). В этом случае мы используем левое соединение основной таблицы с ее табличной частью:


ВЫБРАТЬ
    Абоненты.ТипТелефона КАК ТипТелефона,
    АбонентыДанные.ВидЛьготы КАК ВидЛьготы
ИЗ
    Справочник.Абоненты КАК Абоненты
    ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Абоненты.Данные КАК АбонентыДанные
        ПО АбонентыДанные.Ссылка = Абоненты.Ссылка
ГДЕ
    Абоненты.Ссылка = &Абонент

Использование Временных Таблиц (ВТ)

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


// В первом запросе пакета выбираем ТЧ во Временную Таблицу
ВЫБРАТЬ
    АбонентыДанные.Ссылка КАК Ссылка,
    АбонентыДанные.ВидЛьготы КАК ВидЛьготы
ПОМЕСТИТЬ ВТ_ДанныеАбонентов
ИЗ
    Справочник.Абоненты.Данные КАК АбонентыДанные
ГДЕ
    АбонентыДанные.Ссылка = &Абонент
ИНДЕКСИРОВАТЬ ПО
    Ссылка;

// Во втором запросе пакета соединяем ВТ с другими физическими таблицами
ВЫБРАТЬ
    Абоненты.ТипТелефона КАК ТипТелефона,
    ВТ.ВидЛьготы КАК ВидЛьготы
ИЗ
    Справочник.Абоненты КАК Абоненты
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ДанныеАбонентов КАК ВТ
        ПО ВТ.Ссылка = Абоненты.Ссылка

Использование временных таблиц гарантирует предсказуемость работы оптимизатора СУБД и исключает проблемы с производительностью при работе с большими базами данных.

← На главную