Как выбрать всех контрагентов из иерархической папки в запросе 1С?

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

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

Представим ситуацию: у нас есть справочник Контрагенты, организованный в виде иерархии папок, например, "Покупатели", внутри которой могут быть "Покупатели1", "Покупатели2" и так далее. Нам нужно получить всех контрагентов, которые находятся в папке "Покупатели" и во всех ее вложенных подпапках.

Анализируем исходную проблему: почему не работает простой отбор по родителю

Изначально, у нас мог возникнуть соблазн использовать условие, подобное этому:

ПродажиОбороты.ДоговорКонтрагента.Владелец.Родитель.Наименование = "Покупатели"

Давайте выясним, почему такое условие не принесет желаемого результата.

  1. Ограниченный охват: Условие ПродажиОбороты.ДоговорКонтрагента.Владелец.Родитель.Наименование = "Покупатели" выбирает только тех контрагентов, у которых непосредственным родителем является папка "Покупатели". Это означает, что если у нас есть следующая структура:
    • Папка "Покупатели"
      • Контрагент "Покупатель_А" (лежит непосредственно в "Покупатели")
      • Контрагент "Покупатель_Б" (лежит непосредственно в "Покупатели")
      • Папка "Покупатели1"
        • Контрагент "Покупатель_В" (лежит в "Покупатели1")
    то наше условие выберет только "Покупатель_А" и "Покупатель_Б". Контрагент "Покупатель_В" не будет выбран, потому что его родитель — "Покупатели1", а не "Покупатели".
  2. Ненадежность по наименованию: Отбор по полю Наименование крайне нежелателен. Наименование группы может измениться в любой момент (например, "Покупатели" переименуют в "Клиенты"), и тогда наш запрос перестанет работать, требуя ручных правок. Это создает хрупкость и сложность поддержки системы.
  3. Избыточные "точки" в запросе: Конструкция ПродажиОбороты.ДоговорКонтрагента.Владелец.Родитель.Наименование содержит многократные обращения через точку. Мы рассмотрим это подробнее, но вкратце это приводит к неявным соединениям таблиц в СУБД, что может значительно замедлить выполнение запроса.

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

Правильное решение: оператор В ИЕРАРХИИ

Для выбора всех элементов из заданной группы, включая все ее подгруппы и элементы, находящиеся в них, в языке запросов 1С предусмотрен специальный оператор В ИЕРАРХИИ. Этот оператор разработан специально для работы с иерархическими структурами справочников и регистров.

Давайте посмотрим, как использовать оператор В ИЕРАРХИИ для нашей задачи. Предположим, что в регистре накопления ПродажиОбороты есть измерение ДоговорКонтрагента, и нам нужен Владелец этого договора, который является нашим контрагентом.

Шаг 1: Определяем группу, по которой будем отбирать.

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

Почему по ссылке, а не по наименованию или коду?

  1. Надежность: Ссылки уникальны и стабильны. Наименование может быть изменено, код может быть не уникальным или также изменен.
  2. Производительность: Передача ссылки — это прямое указание на объект в базе данных, что гораздо быстрее, чем поиск объекта по строковому значению (наименованию или коду).

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


&НашПараметрГруппа = Справочники.Контрагенты.НайтиПоНаименованию("Покупатели");
// Или, если у справочника есть своя иерархия групп
// &НашПараметрГруппа = Справочники.Контрагенты.ПапкиКонтрагентов.НайтиПоНаименованию("Покупатели");

Однако, как было отмечено ранее, поиск по наименованию не идеален. Более надежным способом является поиск по уникальному идентификатору (если он известен) или получение ссылки заранее, например, из константы или настройки.

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

&НашПараметрГруппа = Справочники.Контрагенты.НайтиПоКоду("00000009");

Идеальный вариант — это передача уже готовой ссылки, полученной, например, из настроек, или по уникальному идентификатору:


// Если есть уникальный идентификатор
// УИДГруппыПокупатели = Новый УникальныйИдентификатор("ваш УИД здесь");
// &НашПараметрГруппа = Справочники.Контрагенты.ПолучитьПоУникальномуИдентификатору(УИДГруппыПокупатели);

// Или если у вас уже есть ссылка на группу, например, из реквизита формы
// &НашПараметрГруппа = МояФорма.РеквизитГруппаПокупатели;

Шаг 2: Составляем запрос с оператором В ИЕРАРХИИ.

Теперь, когда у нас есть ссылка на группу в переменной &НашПараметрГруппа, мы можем использовать ее в запросе.


ВЫБРАТЬ
    ПродажиОбороты.ДоговорКонтрагента.Владелец КАК Контрагент,
    СУММА(ПродажиОбороты.СуммаОборот) КАК СуммаПродаж
ИЗ
    РегистрНакопления.ПродажиОбороты КАК ПродажиОбороты
ГДЕ
    ПродажиОбороты.ДоговорКонтрагента.Владелец В ИЕРАРХИИ(&НашПараметрГруппа)
СГРУППИРОВАТЬ ПО
    ПродажиОбороты.ДоговорКонтрагента.Владелец

Шаг 3: Устанавливаем параметр запроса.

После создания объекта Запрос, мы должны установить значение параметра &НашПараметрГруппа:


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

// Предположим, мы получили ссылку на группу "Покупатели" каким-то образом
// Например, по коду (хотя лучше по ссылке или УИД)
ПапкаПокупатели = Справочники.Контрагенты.НайтиПоКоду("00000009"); // Пример кода

Если ПапкаПокупатели <> Неопределено Тогда
    Запрос.УстановитьПараметр("НашПараметрГруппа", ПапкаПокупатели);
    РезультатЗапроса = Запрос.Выполнить().Выгрузить();
    // Дальнейшая обработка РезультатЗапроса
Иначе
    Сообщить("Папка 'Покупатели' с кодом 00000009 не найдена.");
КонецЕсли;

Эта конструкция гарантирует, что в выборку попадут все контрагенты, находящиеся в папке, на которую указывает &НашПараметрГруппа, а также во всех ее вложенных подпапках, независимо от уровня вложенности.

Оптимизация запросов и избегание "обращений через точку"

Как мы уже упоминали, избыточные обращения через точку, такие как ПродажиОбороты.ДоговорКонтрагента.Владелец.Родитель.Наименование, могут серьезно замедлить выполнение запроса. Давайте рассмотрим, почему это происходит и как этого избежать.

Почему "точки" замедляют запрос?

Каждое обращение к свойству объекта через точку в запросе (например, .Владелец, .Родитель, .Наименование) на уровне СУБД трансформируется в неявное соединение с другой таблицей. Например, ПродажиОбороты.ДоговорКонтрагента.Владелец означает:

  1. Соединение таблицы регистра ПродажиОбороты с таблицей справочника ДоговорыКонтрагентов (по полю ДоговорКонтрагента).
  2. Затем соединение полученного результата с таблицей справочника Контрагенты (по полю Владелец из ДоговорыКонтрагентов).

Если подобных обращений много, количество соединений может стать критическим, особенно если поля имеют составной тип или отсутствуют подходящие индексы. Это усложняет запрос для оптимизатора СУБД и приводит к его медленному выполнению.

Как оптимизировать запрос?

  1. Используйте прямые измерения: Идеальный вариант — если в вашем регистре накопления (или другом объекте, к которому вы обращаетесь) уже есть прямое измерение, ссылающееся на нужный справочник. Например, если бы в ПродажиОбороты было прямое измерение Контрагент, мы могли бы использовать:
    
    ГДЕ
        ПродажиОбороты.Контрагент В ИЕРАРХИИ(&НашПараметрГруппа)
    

    Это значительно упрощает запрос, так как не требуется дополнительных соединений для получения самого контрагента.

  2. Вынесение сложных путей в выборку: Если прямого измерения нет, и вам все равно нужен сам контрагент, то конструкция ПродажиОбороты.ДоговорКонтрагента.Владелец является необходимой. В данном случае, платформа 1С старается оптимизировать такие запросы, но их все равно стоит использовать разумно.

    Важно: В обсуждаемой теме было уточнено, что конфигурация "не типовая", что может объяснять отсутствие прямого измерения Контрагент в регистре и необходимость обращаться к владельцу договора. В таких случаях мы вынуждены использовать обращения через точку для доступа к нужному полю, но мы всегда должны стремиться к минимизации таких цепочек.

Общие рекомендации по производительности запросов

Помимо рассмотренных выше моментов, давайте затронем и другие важные рекомендации для повышения производительности запросов в 1С.

  1. Использование индексированных полей: Всегда старайтесь использовать условия отбора по полям, которые индексированы. Большинство полей-ссылок, дат, номеров, кодов в справочниках и измерениях регистров по умолчанию индексируются платформой. Сложные выражения или функции в условиях ГДЕ могут препятствовать использованию индексов, заставляя СУБД выполнять полное сканирование таблицы.
  2. Временные таблицы: При работе со сложными запросами, которые включают несколько подзапросов, виртуальные таблицы (например, ОСТАТКИ И ОБОРОТЫ или СрезыПервых/Последних) или требуют множественных соединений, рассмотрите возможность использования временных таблиц.

    Мы можем разбить один большой запрос на несколько меньших, сохраняя промежуточные результаты во временные таблицы (ПОМЕСТИТЬ ИмяВременнойТаблицы) и затем соединяя их. Это позволяет СУБД более эффективно обрабатывать данные, так как она может проиндексировать временные таблицы и выполнить соединения с меньшей нагрузкой.

  3. Избегайте `ИЛИ`: Использование логического оператора ИЛИ в условиях отбора часто приводит к полному сканированию таблицы, так как СУБД может быть сложно использовать индексы для таких условий. Если возможно, переформулируйте запрос, используя оператор В (Поле В (Значение1, Значение2, ...)) или разбейте запрос на несколько частей и объедините результаты через ОБЪЕДИНИТЬ ВСЕ.
  4. Используйте консоль запросов и план запроса: Это ваш лучший друг в оптимизации.
    • Консоль запросов позволяет выполнять запросы на реальных данных, просматривать результаты, замерять время выполнения и видеть структуру временных таблиц.
    • План запроса (доступный через консоль запросов) — это "рентген" того, как СУБД планирует выполнить ваш запрос. Он покажет, какие операции потребляют больше всего ресурсов, порядок обращения к таблицам, использование индексов и количество обработанных строк. Анализ плана запроса часто помогает выявить "узкие места".

Итог

Для того чтобы выбрать всех контрагентов из иерархической папки, включая все вложенные подпапки, мы должны использовать оператор В ИЕРАРХИИ в нашем запросе. Ключевой момент — передавать в качестве параметра запроса прямую ссылку на целевую группу (папку), а не ее наименование или код. Это обеспечит надежность и производительность. Кроме того, всегда стремитесь минимизировать количество "точек" в путях к полям в запросе, используйте прямые измерения регистра, если они доступны, и применяйте временные таблицы для сложных сценариев, чтобы избежать избыточных соединений и улучшить общую производительность запросов.

← На главную