Разработка и Конфигурация · 14 мин
Вложенные запросы в 1С: зачем нужны и как их использовать правильно
Зачем нужны вложенные запросы в 1С, как их писать, когда заменить временными таблицами и какие ошибки приводят к медленным запросам.
Вложенный запрос в 1С — это запрос, помещённый внутрь другого запроса: в секцию источника данных, в условие отбора или в соединение. Внешний запрос работает с результатом внутреннего так же, как с обычной таблицей. Такая конструкция позволяет решать задачи, которые одним «плоским» запросом не выразить: сначала сгруппировать данные, а потом отобрать по итогу группировки; сначала найти максимальное значение, а затем получить строку, где оно достигнуто.
Типичный пример из практики: нужно вывести список клиентов, у которых сумма продаж за период превышает заданный порог. Сначала внутренний запрос группирует обороты по контрагентам и считает сумму, а внешний накладывает условие и подтягивает реквизиты контрагента. Без вложенности пришлось бы либо делать два отдельных запроса с промежуточной обработкой в коде, либо тащить лишние данные в выборку.
При этом вложенные запросы — не единственный и не всегда лучший инструмент. В 1С есть временные таблицы и пакетные запросы, которые часто читаются легче и выполняются быстрее. Ниже разберём, когда вложенность оправдана, как её писать, как проверять результат и в каких случаях стоит выбрать альтернативу.
Что такое вложенный запрос и где он располагается
Синтаксически вложенный запрос заключается в круглые скобки и обязательно получает псевдоним. Встретиться он может в трёх местах. Первое — секция ИЗ, где вложенный запрос выступает источником данных вместо таблицы регистра или справочника. Второе — условие ГДЕ, чаще всего с оператором В: «отобрать документы, номенклатура которых входит в список, полученный другим запросом». Третье — соединения ЛЕВОЕ СОЕДИНЕНИЕ, где вложенный запрос подготавливает агрегированные данные для присоединения к основной выборке.
Внутренний запрос выполняется логически первым: СУБД материализует его результат (реально или виртуально, в зависимости от оптимизатора), и внешний запрос работает уже с этим набором строк. Важно понимать, что во вложенном запросе доступны все возможности языка запросов: группировки, итоги, функции, свои условия и даже собственные вложенные запросы следующего уровня.
Основные сценарии, где вложенный запрос незаменим
Отбор по результату группировки
Классическая задача — найти объекты, у которых агрегированный показатель удовлетворяет условию: контрагенты с суммой долга выше лимита, товары, проданные более чем в N документах, сотрудники с количеством отработанных дней ниже нормы. Внутренний запрос группирует данные и считает агрегат, внешний фильтрует по нему и присоединяет нужные реквизиты.
Получение «последнего» или «первого» значения
Частая задача — найти последнюю цену товара, последний курс валюты, последний документ по контрагенту. Внутренний запрос вычисляет максимальную дату (или максимальный номер) в разрезе измерений, внешний соединяется с исходной таблицей по этой дате и забирает само значение. Это стандартный приём, который без вложенности или временной таблицы не реализуется.
Фильтрация списком из другого запроса
Когда список допустимых значений сам является результатом выборки — например, «номенклатура, по которой были движения за период» — условие ГДЕ Номенклатура В (ВЫБРАТЬ ...) избавляет от промежуточной выгрузки в массив и передачи его параметром. Это особенно удобно, когда список большой: передача тысяч значений параметром типа «массив» работает заметно медленнее.
Проверьте, нельзя ли обойтись ИМЕЮЩИЕ — часто можно
Вложенный запрос или временная таблица — стандартное решение
Условие В (ВЫБРАТЬ ...) быстрее передачи большого массива параметром
Как написать вложенный запрос: разбор на примере
Рассмотрим задачу: выбрать контрагентов, у которых сумма продаж за период больше заданной, вместе с наименованием и ИНН. Запрос выглядит так:
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
Продажи.СуммаПродаж КАК СуммаПродаж,
Контрагенты.ИНН КАК ИНН
ИЗ
(ВЫБРАТЬ
Обороты.Контрагент КАК Контрагент,
СУММА(Обороты.СуммаОборот) КАК СуммаПродаж
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК Обороты
СГРУППИРОВАТЬ ПО
Обороты.Контрагент) КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО Продажи.Контрагент = Контрагенты.Ссылка
ГДЕ
Продажи.СуммаПродаж > &Порог
Порядок написания удобно выстроить «изнутри наружу». Сначала пишется и отлаживается внутренний запрос как самостоятельный — в консоли запросов проверяется, что группировка даёт правильные суммы. Затем он оборачивается скобками, получает псевдоним, и поверх него строится внешняя выборка. Такой подход резко снижает количество ошибок: отлаживать многоуровневый запрос целиком значительно труднее.
Обратите внимание на детали. Псевдоним вложенного запроса обязателен — без него конструктор запросов выдаст ошибку. Поля внутреннего запроса должны иметь явные псевдонимы (КАК СуммаПродаж), иначе обращаться к ним снаружи будет невозможно. Параметры (&НачалоПериода) видны на любом уровне вложенности и передаются один раз.
- 1Сформулируйте, какие данные должен вернуть внутренний запрос, и напишите его как отдельный запрос
- 2Проверьте внутренний запрос в консоли запросов на реальных данных
- 3Оберните его в скобки, добавьте псевдоним и явные имена полей через КАК
- 4Напишите внешний запрос: источник, соединения, условия по полям внутреннего запроса
- 5Проверьте полный запрос на копии базы и сравните итоги с контрольными данными
Вложенный запрос или временная таблица: как выбрать
Обе конструкции решают схожие задачи, но у них разные сильные стороны. Временная таблица создаётся отдельным запросом в пакете с ключевым словом ПОМЕСТИТЬ, а затем используется в последующих запросах пакета. По сути это та же материализация промежуточного результата, только оформленная явно и поименованно.
Вложенный запрос выигрывает, когда логика простая и одноуровневая: одна группировка, один промежуточный результат, используемый один раз. Запрос остаётся компактным и самодостаточным — его легко скопировать в отчёт или обработку. Временная таблица выигрывает, когда промежуточный результат нужен несколько раз в пакете, когда уровней вложенности больше двух, или когда запрос используется в СКД, где читаемость схемы критична.
Есть и практический аргумент по производительности. Оптимизатор СУБД не всегда удачно обрабатывает глубоко вложенные подзапросы — план запроса может оказаться неожиданно тяжёлым. Временная таблица, созданная явно, даёт серверу предсказуемый материализованный набор данных, по которому при необходимости строятся индексы. На больших объёмах (сотни тысяч строк в промежуточном результате) разница заметна.
Сравнение подходов
Подходит для одной-двух ступеней логики и одноразового использования результата. Ограничение: плохо читается при глубокой вложенности, сложнее отлаживать целиком
Подходит для многошаговой логики, повторного использования результата и больших объёмов. Ограничение: требует пакетного запроса и менеджера временных таблиц
Подходит для отбора по агрегату без вложенности вовсе, если реквизиты объекта не нужны или присоединяются отдельно. Ограничение: работает только с полями группировки и агрегатами
Типичные ошибки и как их избежать
Самая распространённая ошибка — забыть, что вложенный запрос в условии В выполняется для всей выборки, и не ограничить его периодом или отбором. Подзапрос по регистру накопления без указания периода виртуальной таблицы заставит СУБД читать весь регистр. Всегда передавайте параметры периода и отборы внутрь подзапроса, а не только во внешний запрос.
Вторая ошибка — дублирование строк при соединении. Если внутренний запрос не сгруппирован до уровня ключа соединения, внешнее соединение умножит строки, и итоги «поплывут». Проверяйте уникальность ключа внутреннего запроса до соединения: одна строка на одно значение ключа.
Третья группа проблем — избыточная вложенность. Три-четыре уровня подзапросов почти всегда признак того, что запрос пора переписать на временные таблицы. Такой код невозможно быстро прочитать, любая правка требует мысленного разбора всех уровней, а вероятность ошибки при доработке растёт многократно.
Производительность: на что обратить внимание
Вложенный запрос сам по себе не замедляет выполнение — медленным его делает неудачный план, который строит СУБД. Чтобы помочь оптимизатору, соблюдайте простые правила. Ограничивайте выборку как можно раньше: условия, относящиеся к данным внутреннего запроса, размещайте внутри него, а не снаружи. Выбирайте во внутреннем запросе только те поля, которые реально используются снаружи — лишние поля увеличивают объём промежуточного результата.
Если запрос работает медленно, первый шаг диагностики — выполнить внутренний запрос отдельно и замерить время и число строк. Если внутренний запрос возвращает миллионы строк, а внешний отбирает из них сотни, значит, условие нужно спустить внутрь. Второй шаг — посмотреть план запроса на стороне СУБД (для MS SQL — через SQL Server Management Studio, для PostgreSQL — через EXPLAIN). Табличное сканирование вместо поиска по индексу — типичный признак проблемы.
Проверка результата
Корректность запроса с вложенностью проверяется поэтапно. Сначала выполните внутренний запрос отдельно и сверьте его итоги с эталоном — например, со стандартным отчётом по тому же регистру за тот же период. Затем выполните полный запрос и проверьте: количество строк соответствует ожиданию, суммы по контрольным группам совпадают с внутренним запросом, нет дублей по ключевым полям.
Отдельно проверьте граничные случаи: пустой результат внутреннего запроса (внешний должен вернуть пустую выборку, а не ошибку), контрагент без движений (при левом соединении поля вложенного запроса будут NULL — учитывайте это в условиях и при выводе), несколько записей с одинаковой максимальной датой (запрос вернёт обе, и это нужно обработать).
Как проверить результат
Суммы совпадают со стандартным отчётом за тот же период
Расхождения — ошибка в группировке или отборе подзапроса
По одной строке на ключевое значение
Дубли — внутренний запрос не сгруппирован до ключа соединения
Секунды на типовом объёме данных
Минуты — проверьте отборы внутри подзапроса и план запроса
NULL обработаны через ЕСТЬNULL или исключены осознанно
NULL попадают в расчёты и условия неявно
Альтернативы вложенным запросам
Прежде чем строить вложенность, проверьте, не решается ли задача проще. Условие ИМЕЮЩИЕ позволяет отобрать группы по агрегату без всякой вложенности — если не нужно присоединять дополнительные реквизиты, это самый короткий путь. Виртуальные таблицы регистров (Обороты, Остатки, СрезПоследних) уже содержат готовую агрегацию — срез последних часто заменяет связку «вложенный запрос с максимумом даты».
В отчётах на СКД промежуточную выборку иногда логичнее вынести в отдельный набор данных или вычисляемое поле, чем разворачивать многоуровневый запрос. А если данные нужны для дальнейшей обработки в коде, временные таблицы с явным менеджером дают максимальную управляемость: их можно дополнять, индексировать (ИНДЕКСИРОВАТЬ ПО) и удалять по мере необходимости.
Как выбрать действие
Частые вопросы
Что важно запомнить
Вложенный запрос нужен, чтобы использовать результат одной выборки как источник или условие другой. Пишите его изнутри наружу, отлаживайте по частям, всегда ограничивайте подзапрос периодом и отборами. При глубокой вложенности и больших объёмах переходите на временные таблицы.
Можно ли вкладывать запрос в запрос несколько раз?
Да, язык запросов 1С допускает несколько уровней вложенности, и технических ограничений на их количество практически нет. Однако уже с третьего уровня запрос становится трудно читать и отлаживать. Если уровней больше двух, почти всегда правильнее разбить логику на временные таблицы в пакетном запросе.
Чем вложенный запрос отличается от временной таблицы?
Вложенный запрос — это подзапрос внутри одного запроса, его результат существует только в момент выполнения. Временная таблица создаётся отдельным запросом пакета через ПОМЕСТИТЬ, хранится в менеджере временных таблиц и доступна всем последующим запросам пакета. Временные таблицы можно индексировать и переиспользовать, вложенные запросы — нет.
Почему запрос с вложенным подзапросом работает медленно?
Чаще всего потому, что подзапрос читает лишние данные: не задан период виртуальной таблицы, условия вынесены во внешний запрос вместо внутреннего, выбираются неиспользуемые поля. Проверьте внутренний запрос отдельно: сколько строк он возвращает и за какое время. Затем посмотрите план запроса на СУБД — сканирование таблиц вместо поиска по индексу укажет на проблемное место.
Когда вместо вложенного запроса достаточно ИМЕЮЩИЕ?
Когда нужно отобрать группы по значению агрегата (СУММА, КОЛИЧЕСТВО, МАКСИМУМ) и не требуется присоединять дополнительные данные к результату. Например, «контрагенты с суммой продаж выше порога» — это одна группировка с ИМЕЮЩИЕ. Но если к результату нужно добавить ИНН, адрес и руководителя, потребуется соединение, и вложенный запрос становится оправданным.
Можно ли использовать вложенные запросы в СКД?
Да, в наборе данных типа «Запрос» вложенные запросы работают без ограничений. Но в СКД особенно важна читаемость: схему часто дорабатывают другие разработчики. Если логика сложная, лучше использовать несколько наборов данных или временные таблицы в тексте запроса схемы — это упрощает поддержку отчёта.
Видны ли параметры внешнего запроса внутри вложенного?
Да, параметры (например, &НачалоПериода) доступны на любом уровне вложенности и передаются в запрос один раз через УстановитьПараметр. Это удобно: один и тот же период можно использовать и внутри подзапроса для отбора данных, и снаружи для итогового условия.