Как устроен вложенный запрос в 1С и когда его лучше заменить

· 9 мин

Содержание

Вложенный запрос в 1С — это запрос внутри другого запроса. Стоит он в одном из двух мест: источником в секции ИЗ или внутри условия через оператор В. Результат такого запроса нигде не сохраняется — сервер пересчитывает его при каждом выполнении внешнего запроса.

Два способа получить промежуточную выборку в языке запросов 1С — вложенный запрос и временная таблица. Первый компактнее и годится для небольших наборов. Вторая выполняется быстрее на крупных данных, потому что её «видит» оптимизатор сервера СУБД. Где между ними проходит граница — разберём на примерах с кодом.

Статья про язык запросов платформы 1С:Предприятие 8.3 — одинаково для Бухгалтерии, ЗУП и УТ, конфигурация роли не играет.

Где можно поставить вложенный запрос

В языке запросов 1С вложенный запрос разрешён не везде. Мест ровно два:

  • 🔹 В секции ИЗ — как виртуальный источник данных. Обязательно с псевдонимом через КАК.
  • 🔹 В условии ГДЕ через оператор В — проверка вхождения значения в результат подзапроса.

А вот чего в языке запросов 1С нет:

  • ❌ Нельзя поставить подзапрос прямо в список полей ВЫБРАТЬ (скалярный подзапрос СУБД здесь не поддерживается).
  • ❌ Нет коррелированных подзапросов — вложенный запрос не может сослаться на поле текущей строки внешнего запроса.

Эти два ограничения важны. Если в обычном SQL вы привыкли писать (SELECT SUM(...) FROM ... WHERE t.id = outer.id) в списке колонок — в 1С так не получится. Задача решается соединением (СОЕДИНЕНИЕ) с сгруппированным подзапросом или через временную таблицу.

Синтаксис и обязательные псевдонимы

Вложенный запрос как источник пишут в круглых скобках, за которыми идёт КАК и имя. Без псевдонима платформа выдаст ошибку синтаксиса.

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

Что здесь важно по синтаксису:

  1. У каждого поля внутри подзапроса — свой псевдоним (КАК Номенклатура, КАК ВсегоКоличество). Снаружи обращаться можно только к этим именам.
  2. У самого подзапроса — псевдоним Продажи.
  3. Внешний запрос видит подзапрос как обычную таблицу с колонками Номенклатура и ВсегоКоличество.

Второй вариант — подзапрос в условии. Здесь псевдоним не нужен, потому что подзапрос возвращает один столбец значений для проверки В:

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

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

Область видимости полей и параметров

У вложенного и внешнего запросов — разные «миры». Правила простые, но именно на них спотыкаются чаще всего.

Поля подзапроса наружу не видны напрямую. Внешний запрос обращается к ним только через псевдоним подзапроса. Строка Товары.Количество во внешней части из примера выше работать не будет — снаружи существует только Продажи.ВсегоКоличество.

Внутрь заглянуть тоже нельзя. Подзапрос не знает про таблицы и псевдонимы внешнего запроса. Сослаться на строку внешнего запроса из подзапроса невозможно — это и есть отсутствие коррелированных подзапросов.

А вот параметры видны везде. Параметр &Имя в языке запросов 1С имеет глобальную область видимости на весь запрос, включая все уровни вложенности и все пакеты. Задаёте его один раз через Запрос.УстановитьПараметр(...), а пользоваться можно хоть в самом глубоком подзапросе.

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

Параметр &ДатаОстатка объявлен внутри подзапроса, но установить его нужно на уровне всего объекта запроса. Это удобно: один параметр — одно значение на весь текст, без дублирования.

Чем заменить коррелированный подзапрос

Раз подзапрос не видит строку внешнего запроса, типовая задача из SQL — «для каждого элемента посчитать его сумму/последнее значение» — решается иначе. Берём сгруппированный подзапрос и соединяем его левым соединением по ключу.

Например, для каждой номенклатуры из справочника — её общее проданное количество:

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

Разберём приём:

  • Подзапрос Продажи агрегирует продажи по номенклатуре один раз, а не по строке на каждый элемент.
  • ЛЕВОЕ СОЕДИНЕНИЕ сохраняет все позиции справочника, даже те, что ни разу не продавались.
  • ЕСТЬNULL(..., 0) подставляет ноль вместо NULL там, где продаж не было.

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

Как сервер выполняет вложенный запрос

Язык запросов 1С — это не SQL СУБД. Платформа транслирует ваш запрос в SQL для MS SQL Server или PostgreSQL, а уже сервер базы данных строит план выполнения.

Вложенный запрос превращается в производную таблицу (derived table) — подзапрос прямо в тексте SQL. И вот ключевой момент: у результата вложенного запроса нет статистики и нет индексов. Оптимизатор сервера не знает заранее, сколько строк вернёт подзапрос и как они распределены.

Что из этого следует:

  • На небольших наборах оптимизатор часто «разворачивает» (inline) подзапрос во внешний и строит нормальный план — тогда вложенный запрос ничем не хуже.
  • На больших объёмах сервер может выбрать неудачный способ соединения (вложенные циклы вместо хеш-соединения) и дать сканирование всей таблицы.
  • Чем глубже вложенность и чем больше соединений виртуальных наборов между собой, тем выше риск, что план «поплывёт».

Самый тяжёлый случай — соединение двух вложенных запросов между собой или соединение вложенного запроса с виртуальной таблицей регистра (остатки, обороты). У обоих наборов нет индексов, и сервер вынужден перебирать строки без опоры на статистику.

Вложенный запрос или временная таблица

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

Тот же пример с продажами, переписанный на временную таблицу:

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

ВЫБРАТЬ
    ВтПродажи.Номенклатура,
    ВтПродажи.ВсегоКоличество
ИЗ
    ВтПродажи КАК ВтПродажи
ГДЕ
    ВтПродажи.ВсегоКоличество > 100

Запросы в пакете разделяются точкой с запятой. Временная таблица ВтПродажи живёт до конца работы менеджера временных таблиц и доступна во всех следующих запросах пакета по имени.

Сравнение по существу:

КритерийВложенный запросВременная таблица
Хранение результатане хранится, пересчёт каждый разматериализуется в базе
Статистика для оптимизаторанетесть
ИндексынельзяИНДЕКСИРОВАТЬ ПО
Повторное использованиенет, только в месте вставкида, во всём пакете
Объём кодакомпактнеедлиннее (отдельный запрос)
Когда выигрываетнебольшие наборы, разовое использованиебольшие данные, соединения, повторное обращение

Практический критерий выбора:

Вложенный запрос или временная таблица — что ставить?

Где вложенные запросы начинают тормозить

Несколько шаблонов, которые стабильно дают медленные запросы. Если видите такой в коде — это кандидат на переписывание.

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

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

ГДЕ
    Номенклатура.Ссылка В (&СписокНоменклатуры)

3. Соединение вложенного запроса с виртуальной таблицей. Виртуальные таблицы регистров (Остатки, Обороты, ОстаткиИОбороты) сами по себе тяжёлые. Соединять их с ещё одним неиндексированным подзапросом — двойная нагрузка.

4. Глубокая вложенность. Подзапрос внутри подзапроса внутри подзапроса. Читать такое трудно, а оптимизатору тяжело построить план. Разбейте на пакет временных таблиц — и быстрее, и понятнее.

Как увидеть проблему на практике:

  • В конфигураторе или через расширение — технологический журнал и замер производительности (ЗамерПроизводительности).
  • Длительность запроса в клиент-серверном режиме смотрят в плане выполнения на стороне СУБД (MS SQL — через SQL Server Profiler или план запроса; PostgreSQL — EXPLAIN ANALYZE).
  • Признак беды — сканирование таблицы целиком там, где ожидался поиск по индексу.

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

Собранные правила, которые держат запросы быстрыми и читаемыми.

  1. Глубина вложенности — не больше одного-двух уровней. Всё, что глубже, выносите в пакет временных таблиц.
  2. Соединяете промежуточный набор с чем-то ещё — делайте временную таблицу и индексируйте по полям соединения через ИНДЕКСИРОВАТЬ ПО.
  3. Не соединяйте два виртуальных набора (подзапрос с подзапросом, подзапрос с виртуальной таблицей регистра) напрямую — хотя бы один из них должен быть индексированной временной таблицей.
  4. В (ВЫБРАТЬ ...) на больших данных заменяйте на временную таблицу или на параметр-массив В (&Список).
  5. Фильтруйте как можно раньше. Условия ГДЕ ставьте внутри подзапроса, чтобы он возвращал меньше строк, а не отсекайте лишнее снаружи.
  6. Давайте полям и подзапросам осмысленные псевдонимы — через месяц читать будет проще.

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

Проверка вложенного запроса перед сдачей
Выполнено: 0 / 6

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

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