Как использовать вложенные подзапросы в языке запросов 1С

· 9 мин

Содержание

Подзапрос в языке запросов 1С — это обычный запрос ВЫБРАТЬ ... ИЗ ..., заключённый в круглые скобки и вставленный внутрь другого запроса. Он работает в трёх местах: как источник данных в секции ИЗ, как условие отбора в секции ГДЕ (через оператор В) и как вычисляемое значение прямо в списке полей после ВЫБРАТЬ.

Главное правило, из-за которого запрос чаще всего не компилируется: подзапрос в секции ИЗ обязан иметь псевдоним, а каждое вычисляемое поле внутри него — тоже. Ниже разберём синтаксис всех трёх вариантов, разницу между коррелированными и автономными подзапросами и то, почему на больших данных подзапрос почти всегда стоит заменить временной таблицей.

Как устроен подзапрос: скобки, псевдонимы, вложенность

Синтаксически подзапрос ничем не отличается от самостоятельного запроса — те же секции ВЫБРАТЬ, ИЗ, ГДЕ, СГРУППИРОВАТЬ ПО. Разница в том, что он заключён в круглые скобки и его результат использует внешний запрос.

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

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

Обратите внимание на две вещи. Во-первых, у подзапроса в конце стоит КАК Продажи — это имя, по которому внешний запрос обращается к его полям. Во-вторых, у агрегата СУММА(Строки.Сумма) есть псевдоним КАК СуммаПродаж, иначе поле окажется безымянным и внешний ВЫБРАТЬ его «не увидит».

Вложенность не ограничена одним уровнем: подзапрос может содержать свой подзапрос. На практике больше двух-трёх уровней — сигнал, что запрос пора разбивать на временные таблицы: читать и отлаживать такую конструкцию почти невозможно.

Подзапрос в секции ИЗ как отдельная таблица

Это самый частый сценарий. Подзапрос в ИЗ ведёт себя как временная таблица, существующая только во время выполнения запроса. Её удобно соединять с другими таблицами и отбирать по ней данные.

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

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

Здесь важен ЕСТЬNULL(Продажи.Количество, 0): при левом соединении у товара без продаж поле Продажи.Количество окажется NULL, и без обёртки ЕСТЬNULL любая арифметика с ним даст пустое значение.

Соединение с подзапросом — именно то место, где запросы начинают тормозить. Внешний оптимизатор СУБД не знает, сколько строк вернёт подзапрос, и часто выбирает неудачный план. Если во внешнем соединении участвует подзапрос с группировкой, держите в голове возможность заменить его временной таблицей — об этом ниже.

Подзапрос в условии ГДЕ через оператор В

Второй способ — отобрать записи по списку, который возвращает подзапрос. В 1С для этого служит оператор В (и его отрицание НЕ В). Отдельного оператора EXISTS, как в чистом SQL, в языке запросов 1С нет — его роль играет В с подзапросом.

Отберём документы, в которых встречается хотя бы один товар из заданной группы:

ВЫБРАТЬ
    Реализация.Ссылка КАК Документ
ИЗ
    Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
    Реализация.Ссылка В
        (ВЫБРАТЬ РАЗЛИЧНЫЕ
            Товары.Ссылка
        ИЗ
            Документ.РеализацияТоваровУслуг.Товары КАК Товары
        ГДЕ
            Товары.Номенклатура.Родитель = &Группа)

Подзапрос в В должен возвращать ровно одну колонку — тот тип данных, с которым сравнивается левое поле. РАЗЛИЧНЫЕ здесь убирает дубли ссылок, чтобы список не разрастался.

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

ВЫБРАТЬ
    Товары.Ссылка КАК Номенклатура
ИЗ
    Справочник.Номенклатура КАК Товары
ГДЕ
    НЕ Товары.ЭтоГруппа
    И НЕ Товары.Ссылка В
        (ВЫБРАТЬ РАЗЛИЧНЫЕ Оборот.Номенклатура
         ИЗ РегистрНакопления.ТоварыНаСкладах.Обороты КАК Оборот)

У НЕ В есть коварная особенность: если подзапрос вернёт хотя бы одно значение NULL, всё условие НЕ В даст пустой результат и внешний запрос вернёт ноль строк. Отсеивайте NULL в подзапросе условием ГДЕ Оборот.Номенклатура ЕСТЬ НЕ NULL или следите, чтобы поле было непустым по смыслу.

Оператор В ИЕРАРХИИ принимает подзапрос так же, как В, но дополнительно разворачивает группы справочника до вложенных элементов — удобно, когда список групп-отборов тоже приходит запросом.

Третий, менее очевидный вариант — подзапрос как скалярное значение прямо в списке полей ВЫБРАТЬ. Тогда он обязан возвращать одну строку и одну колонку:

ВЫБРАТЬ
    Контрагенты.Наименование КАК Контрагент,
    (ВЫБРАТЬ СУММА(Долг.СуммаОстаток)
     ИЗ РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки КАК Долг
     ГДЕ Долг.Контрагент = Контрагенты.Ссылка) КАК Долг
ИЗ
    Справочник.Контрагенты КАК Контрагенты

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

Коррелированные и автономные подзапросы — в чём разница

Подзапросы делятся на два вида по тому, ссылаются ли они на поля внешнего запроса.

  • Автономный (некоррелированный) подзапрос самодостаточен. Он не упоминает таблицы внешнего запроса и вычисляется один раз. Все примеры в секциях ИЗ выше — автономные.
  • Коррелированный подзапрос содержит ссылку на внешнюю таблицу (в последнем примере — Долг.Контрагент = Контрагенты.Ссылка). СУБД вынуждена пересчитывать его заново для каждой строки внешнего запроса.

Разница в скорости огромна. Если во внешнем запросе 10 000 контрагентов, коррелированный подзапрос отработает 10 000 раз. Автономный подзапрос с последующим соединением посчитает остатки один раз и соединит их за один проход.

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

Перепишем пример с долгом без корреляции — считаем остатки один раз, затем соединяем:

ВЫБРАТЬ
    Контрагенты.Наименование КАК Контрагент,
    ЕСТЬNULL(Долги.Сумма, 0) КАК Долг
ИЗ
    Справочник.Контрагенты КАК Контрагенты
    ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
        Ост.Контрагент КАК Контрагент,
        СУММА(Ост.СуммаОстаток) КАК Сумма
    ИЗ
        РегистрНакопления.ВзаиморасчетыСКонтрагентами.Остатки КАК Ост
    СГРУППИРОВАТЬ ПО
        Ост.Контрагент) КАК Долги
    ПО Долги.Контрагент = Контрагенты.Ссылка

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

Правила псевдонимов и ограничения подзапросов

У подзапросов есть ограничения, которые язык запросов 1С проверяет на этапе компоновки текста. Нарушение любого из них — ошибка ещё до обращения к базе.

ПравилоЧто именно
Псевдоним источникаПодзапрос в ИЗ обязан заканчиваться на КАК ИмяТаблицы. Без имени — ошибка синтаксиса.
Псевдонимы вычисляемых полейЛюбое поле-выражение внутри подзапроса (СУММА(...), ВЫБОР КОГДА ..., конкатенация) требует КАК ИмяПоля.
Одна колонка для ВПодзапрос в операторе В возвращает строго одну колонку сравнимого типа.
Одно значение для поляПодзапрос в списке полей ВЫБРАТЬ должен вернуть одну строку и одну колонку.
УПОРЯДОЧИТЬ ПО — только с ПЕРВЫЕСортировать внутри подзапроса разрешено лишь вместе с ПЕРВЫЕ N. Самостоятельная сортировка подзапроса смысла не имеет и запрещена.
Нет ИТОГИ и ДЛЯ ИЗМЕНЕНИЯСекции ИТОГИ ПО и ДЛЯ ИЗМЕНЕНИЯ в подзапросах недопустимы — они работают только во внешнем запросе.

Отдельно про обращение к полям. Внешний запрос видит только то, что подзапрос явно вывел в своей секции ВЫБРАТЬ, и обращается к полям через псевдоним подзапроса: Продажи.Номенклатура, а не Строки.Номенклатура. Внутреннее имя таблицы (Строки) за скобками подзапроса не существует.

Полезный приём: выбор ПЕРВЫЕ 1 с УПОРЯДОЧИТЬ ПО внутри коррелированного подзапроса даёт «последнюю цену», «последний документ по контрагенту» и подобные значения. Но на больших данных это снова коррелированный подзапрос — проверьте, нельзя ли то же получить через СрезПоследних регистра сведений.

Производительность: подзапрос или временная таблица

Короткий ответ практика: если подзапрос с группировкой или фильтрацией участвует в соединении и работает с большими объёмами — выносите его в временную таблицу. Платформа соберёт её, проиндексирует, передаст СУБД статистику по числу строк, и оптимизатор построит адекватный план.

Временная таблица создаётся секцией ПОМЕСТИТЬ:

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

ВЫБРАТЬ
    Остатки.Номенклатура КАК Номенклатура,
    Остатки.КоличествоОстаток КАК Остаток,
    ЕСТЬNULL(ВТ_Продажи.Количество, 0) КАК Продано
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Продажи
    ПО Остатки.Номенклатура = ВТ_Продажи.Номенклатура

Два запроса разделены точкой с запятой — это пакетный запрос. Чтобы временная таблица жила между ними, объекту Запрос нужно назначить менеджер временных таблиц:

МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст = ТекстПакетногоЗапроса;
Результат = Запрос.Выполнить();

ИНДЕКСИРОВАТЬ ПО в примере строит индекс по полю соединения — без него соединение большой временной таблицы снова упрётся в перебор.

Когда временная таблица оправдана, а когда нет:

  • Выносить в ВТ — если подзапрос с группировкой соединяется с другими таблицами, если один и тот же подзапрос нужен в нескольких местах, если данных много.
  • Оставить подзапросом — простой автономный подзапрос в В на небольшом наборе, одноразовый подзапрос-источник без тяжёлых соединений.

Ещё два ограничения, о которые спотыкаются при переходе с подзапросов на пакет. Во-первых, ПОМЕСТИТЬ нельзя писать внутри подзапроса — временная таблица создаётся только самостоятельным запросом пакета. Во-вторых, соединять с временной таблицей по полю, которое не вошло в ИНДЕКСИРОВАТЬ ПО, бессмысленно: выигрыш от индекса появляется ровно по тем полям, которые в нём перечислены, поэтому индексируйте именно поля соединений и отборов.

Посмотреть, что на самом деле выполняет СУБД, можно через план выполнения запроса. Текст запроса к СУБД снимается технологическим журналом (события DBMSSQL и SDBL), а сам план — средствами базы данных: в MS SQL Server через профилировщик или SET SHOWPLAN, в PostgreSQL через EXPLAIN ANALYZE. Для оценки длительности в разрезе ключевых операций удобна «Консоль запросов» с диска ИТС и замер производительности в конфигураторе.

Типичные ошибки при работе с подзапросами

Большинство ошибок с подзапросами ловятся по тексту сообщения. Вот как их читать.

Диагностика: выберите симптом

Ещё две ошибки, которые не дают сообщения, но портят результат:

  1. Потеря строк из-за NULL. После левого соединения с подзапросом поля несопоставленных строк равны NULL. Отбор вида ГДЕ Продажи.Количество > 0 молча выкинет товары без продаж. Оборачивайте такие поля в ЕСТЬNULL(...) до сравнения.
  2. Дубли из-за соединения. Если подзапрос вернул по ключу несколько строк, соединение размножит записи внешнего запроса. Проверяйте, что подзапрос сгруппирован по полю соединения и ключ в нём уникален.

Частые вопросы

Чем подзапрос отличается от соединения?
Соединение (СОЕДИНЕНИЕ ... ПО) сопоставляет строки двух источников. Подзапрос — это вложенный запрос, результат которого используется как источник, как условие или как значение. Часто одну задачу можно решить обоими способами; выбор делают по читаемости и скорости.
Можно ли обойтись без псевдонима у подзапроса?
Нет. Подзапрос в секции ИЗ всегда требует имени через КАК, а каждое вычисляемое поле внутри него — своего псевдонима. Это проверяется ещё до обращения к базе.
Сколько уровней вложенности допустимо?
Формального предела нет, но после двух-трёх уровней запрос становится нечитаемым и медленным. Практичнее разбить его на пакет с временными таблицами.
Почему запрос с подзапросом работает быстро в консоли и медленно в документе?
В консоли вы обычно гоняете его на малой выборке или с другими параметрами. На боевых объёмах всплывают коррелированные подзапросы и соединения с неиндексированными подзапросами. Снимайте план выполнения на реальных данных.
Когда подзапрос точно лучше заменить временной таблицей?
Когда подзапрос с группировкой участвует в соединении, когда он нужен в нескольких местах пакета или когда работает с большими регистрами. Временная таблица с ИНДЕКСИРОВАТЬ ПО даёт оптимизатору статистику и обычно ускоряет запрос в разы.