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

Если вы когда-нибудь сталкивались с ситуацией, когда простой на первый взгляд отчёт выполняется несколько минут, а анализ плана запроса показывает десятки вложенных SELECT-ов, то эта статья для вас. Мы разберём не только техническую сторону вложенных запросов (синтаксис, особенности работы с СУБД), но и практические сценарии, где они действительно необходимы. А ещё — покажем, как одно неудачное решение может превратить быструю выборку в тормозящий кошмар.

Спойлер: в 90% случаев вложенные запросы можно заменить на более эффективные конструкции. Но оставшиеся 10% как раз и делают их ценным инструментом в арсенале 1С-разработчика.

Что такое вложенный запрос в 1С и как он работает

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

ВЫБРАТЬ

Товары.Наименование

ИЗ

Справочник.Товары КАК Товары

ГДЕ

Товары.Ссылка В (

ВЫБРАТЬ

ТоварыВЗаказе.Товар

ИЗ

Документ.ЗаказПокупателя.Товары КАК ТоварыВЗаказе

ГДЕ

ТоварыВЗаказе.Заказ = &ТекущийЗаказ

)

На уровне СУБД (например, Microsoft SQL Server или PostgreSQL) такой запрос преобразуется в классический SQL с подзапросами. Однако здесь кроется первая ловушка: 1С не всегда оптимизирует вложенные запросы так, как это делает «чистый» SQL. В результате вместо ожидаемого ускорения вы можете получить многократное увеличение времени выполнения.

Ключевые особенности вложенных запросов в 1С:

  • 🔹 Ленивое выполнение: подзапрос выполняется для каждой строки внешнего запроса (если не оптимизирован СУБД). Это критично для больших выборок.
  • 🔹 Ограниченная оптимизация: платформа 1С не всегда может «развернуть» вложенный запрос в соединение (JOIN).
  • 🔹 Контекст выполнения: вложенный запрос «не знает» о полях внешнего, если они не переданы через параметры.
  • 🔹 Поддержка коррелированных подзапросов: когда внутренний запрос ссылается на поля внешнего (например, ГДЕ ВнутренняяТаблица.Поле = ВнешняяТаблица.Поле).

Важно понимать, что вложенные запросы — это не синоним «плохого кода». Они становятся проблемой только при неграмотном использовании. Например, если вы выбираете 10 000 строк из внешнего запроса, а для каждой из них выполняется вложенный запрос к таблице с миллионом записей, то даже самая мощная СУБД будет «думать» долго.

📊 Как часто вы используете вложенные запросы в 1С?
Постоянно, это мой основной инструмент
Только когда без них не обойтись
Раньше использовал, но теперь заменяю на JOIN
Никогда не использую, только временные таблицы

Когда вложенные запросы действительно нужны: 5 практических кейсов

Несмотря на риски, есть ситуации, где вложенные запросы — самое элегантное и эффективное решение. Рассмотрим реальные примеры из практики 1С-разработчиков.

1. Проверка существования связанных данных без JOIN

Допустим, вам нужно выбрать контрагентов, у которых есть хотя бы один неоплаченный счёт. Классический JOIN вернёт дублирующиеся строки, если у контрагента несколько счетов. Вложенный запрос решает эту задачу лаконично:

ВЫБРАТЬ

Контрагенты.Наименование

ИЗ

Справочник.Контрагенты КАК Контрагенты

ГДЕ

ИМЕЕТСЯ (

ВЫБРАТЬ

Счета.Контрагент

ИЗ

Документ.СчетНаОплату КАК Счета

ГДЕ

Счета.Контрагент = Контрагенты.Ссылка

И Счета.Статус = Перечисление.СтатусыСчетов.НеОплачен

)

Альтернатива — использование РАЗЛИЧНЫЕ или ГРУППИРОВКА, но они менее читаемы и могут быть медленнее на больших объёмах данных.

2. Коррелированные подзапросы для сложных условий

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

ВЫБРАТЬ

Товары.Наименование,

Товары.Цена

ИЗ

Справочник.Товары КАК Товары

ГДЕ

Товары.Цена > (

ВЫБРАТЬ

СРЕДНЕЕ(ТоварыКатегории.Цена)

ИЗ

Справочник.Товары КАК ТоварыКатегории

ГДЕ

ТоварыКатегории.Категория = Товары.Категория

)

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

3. Динамическая фильтрация по результатам другого запроса

Допустим, вам нужно выбрать документы, дата которых попадает в диапазон, рассчитанный на основе других данных. Например, «выбрать заказы за последний квартал, но только для тех клиентов, кто сделал хотя бы один заказ в прошлом году». Вложенный запрос позволяет сделать это в одном выражении:

ВЫБРАТЬ

Заказы.Номер,

Заказы.Дата

ИЗ

Документ.ЗаказПокупателя КАК Заказы

ГДЕ

Заказы.Клиент В (

ВЫБРАТЬ РАЗЛИЧНЫЕ

ЗаказыПрошлогоГода.Клиент

ИЗ

Документ.ЗаказПокупателя КАК ЗаказыПрошлогоГода

ГДЕ

ЗаказыПрошлогоГода.Дата МЕЖДУ &НачалоПрошлогоГода И &КонецПрошлогоГода

)

И Заказы.Дата МЕЖДУ &НачалоТекущегоКвартала И &КонецТекущегоКвартала

4. Работа с иерархическими данными

Вложенные запросы удобны для обхода иерархий, например, когда нужно выбрать элементы справочника, у которых есть дочерние элементы с определённым признаком. Пример: «найти группы номенклатуры, в которых есть хотя бы один товар с нулевым остатком».

ВЫБРАТЬ

Группы.Наименование

ИЗ

Справочник.Номенклатура КАК Группы

ГДЕ

Группы.ЭтотОбъектГруппа = ИСТИНА

И ИМЕЕТСЯ (

ВЫБРАТЬ

Товары.Ссылка

ИЗ

Справочник.Номенклатура КАК Товары

ГДЕ

Товары.Родитель = Группы.Ссылка

И Товары.Остаток = 0

)

5. Оптимизация «тяжёлых» отчётов с предварительной фильтрацией

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

ВЫБРАТЬ

Продажи.Товар,

СУММА(Продажи.Количество) КАК Количество

ИЗ

Документ.РеализацияТоваров.Товары КАК Продажи

ГДЕ

Продажи.Товар В (

ВЫБРАТЬ ПЕРВЫЕ 100

ТопТовары.Товар

ИЗ

(

ВЫБРАТЬ

ТоварыТоп.Товар,

СУММА(ТоварыТоп.Количество) КАК Количество

ИЗ

Документ.РеализацияТоваров.Товары КАК ТоварыТоп

ГДЕ

ТоварыТоп.Документ.Дата МЕЖДУ &НачалоМесяца И &КонецМесяца

СГРУППИРОВАТЬ ПО

ТоварыТоп.Товар

УПОРЯДОЧИТЬ ПО

Количество УБЫВ

) КАК ТопТовары

)

СГРУППИРОВАТЬ ПО

Продажи.Товар

В этом примере вложенный запрос сначала отбирает топ-100 товаров, а внешний — уже работает только с этой выборкой, а не со всей таблицей реализаций.

💡

Вложенные запросы оправданы, когда они сокращают объём обрабатываемых данных на ранних этапах или позволяют выразить логику, которую сложно реализовать иначе (например, коррелированные условия).

Вложенные запросы vs. временные таблицы vs. объединения (JOIN): что быстрее?

Главный вопрос, который волнует разработчиков: какой подход даст лучшую производительность? Ответ зависит от объёма данных, структуры запроса и возможностей СУБД. Давайте сравним три основных альтернативы.

Критерий Вложенные запросы Временные таблицы Объединения (JOIN)
Производительность на больших данных ❌ Медленно (если не оптимизировано СУБД) ✅ Быстро (данные предварительно отфильтрованы) ✅ Быстро (при правильных индексах)
Читаемость кода ✅ Лаконично для простых условий ❌ Много кода (создание, заполнение, использование) ✅ Понятно при небольшом числе соединений
Гибкость ✅ Позволяет динамические условия ✅ Можно использовать в нескольких запросах ❌ Сложно выразить коррелированные условия
Память ✅ Не требует дополнительной памяти ❌ Занимает память под временные данные ✅ Не требует дополнительной памяти
Поддержка в 1С ✅ Полная ✅ Полная ✅ Полная (но ограничения на LEFT JOIN в старых версиях)

На практике выбор зависит от конкретной задачи:

  • 🔍 Вложенные запросы удобны для простых фильтров (например, ГДЕ Поле В (подзапрос)) или когда нужно проверить существование связанных данных.
  • 📊 Временные таблицы выигрывают, если один и тот же набор данных используется несколько раз в отчёте или если нужно предварительно агрегировать данные.
  • 🔗 JOIN оптимален для связывания таблиц по ключам, особенно если нужны данные из нескольких источников.

Пример, где JOIN проигрывает вложенному запросу: выборка клиентов, у которых нет ни одного заказа за год. С JOIN пришлось бы использовать LEFT JOIN ... WHERE IS NULL, что менее интуитивно, чем:

ВЫБРАТЬ

Клиенты.Наименование

ИЗ

Справочник.Клиенты КАК Клиенты

ГДЕ

НЕ Клиенты.Ссылка В (

ВЫБРАТЬ РАЗЛИЧНЫЕ

Заказы.Клиент

ИЗ

Документ.ЗаказПокупателя КАК Заказы

ГДЕ

Заказы.Дата >= &НачалоГода

)

💡

Если вложенный запрос возвращает большое количество строк (более 10% от внешней выборки), скорее всего, его стоит заменить на временную таблицу или JOIN.

Типичные ошибки при работе с вложенными запросами (и как их избежать)

Даже опытные разработчики иногда допускают ошибки, которые превращают вложенные запросы в «узкое горлышко» производительности. Разберём самые распространённые из них.

1. Вложенный запрос в секции ВЫБРАТЬ

Никогда не пишите так:

ВЫБРАТЬ

Заказы.Номер,

(

ВЫБРАТЬ

СУММА(Товары.Сумма)

ИЗ

Документ.ЗаказПокупателя.Товары КАК Товары

ГДЕ

Товары.Ссылка = Заказы.Ссылка

) КАК СуммаЗаказа

ИЗ

Документ.ЗаказПокупателя КАК Заказы

Проблема: такой запрос выполнит подзапрос для каждой строки внешней выборки. Если в таблице 10 000 заказов, то вложенный запрос выполнится 10 000 раз!

Решение: используйте JOIN с группировкой или временную таблицу.

2. Коррелированные подзапросы без индексов

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

ВЫБРАТЬ

Товары.Наименование

ИЗ

Справочник.Товары КАК Товары

ГДЕ

Товары.Цена > (

ВЫБРАТЬ

СРЕДНЕЕ(Аналоги.Цена)

ИЗ

Справочник.Товары КАК Аналоги

ГДЕ

Аналоги.Категория = Товары.Категория -- Корреляция!

)

Если поле Категория не проиндексировано, СУБД будет сканировать всю таблицу для каждой строки внешнего запроса.

3. Вложенные запросы в циклах

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

Выборка = Запрос.Выполнить();

Пока Выборка.Следующий() Цикл

Подзапрос = Новый Запрос;

Подзапрос.Текст = "

ВЫБРАТЬ

СУММА(Движения.Количество)

ИЗ

РегистрНакопления.ОстаткиТоваров КАК Движения

ГДЕ

Движения.Товар = &Товар";

Подзапрос.УстановитьПараметр("Товар", Выборка.Товар);

Результат = Подзапрос.Выполнить();

КонецЦикла;

Проблема: вместо одного запроса к базе вы делаете сотни! Это одна из главных причин «тормозов» в 1С.

Решение: перенесите логику в один запрос с JOIN или используйте временные таблицы.

4. Избыточная вложенность

Чем глубже уровень вложенности, тем сложнее СУБД оптимизировать запрос. Например:

ВЫБРАТЬ

Клиенты.Наименование

ИЗ

Справочник.Клиенты КАК Клиенты

ГДЕ

Клиенты.Ссылка В (

ВЫБРАТЬ

Заказы.Клиент

ИЗ

Документ.ЗаказПокупателя КАК Заказы

ГДЕ

Заказы.Ссылка В (

ВЫБРАТЬ

Оплаты.Заказ

ИЗ

Документ.Оплата КАК Оплаты

ГДЕ

Оплаты.Сумма > 10000

)

)

Проблема: три уровня вложенности усложняют план выполнения. СУБД может не «развернуть» такой запрос в эффективный JOIN.

Решение: замените на цепочку JOIN-ов или временные таблицы.

5. Использование В (подзапрос) вместо СУЩЕСТВУЕТ

Если вам нужно проверить существование связанных данных, используйте СУЩЕСТВУЕТ вместо В:

-- Медленно (возвращает все ссылки, а потом проверяет вхождение)

ГДЕ Товар В (ВЫБРАТЬ ТоварыВЗаказе.Товар ИЗ ...)

-- Быстрее (останавливается на первой найденной строке)

ГДЕ СУЩЕСТВУЕТ (

ВЫБРАТЬ 1

ИЗ Документ.ЗаказПокупателя.Товары КАК ТоварыВЗаказе

ГДЕ ТоварыВЗаказе.Товар = Товары.Ссылка

)

СУЩЕСТВУЕТ прекращает выполнение подзапроса при первом совпадении, тогда как В сначала собирает все данные.

Почему вложенные запросы иногда работают быстрее JOIN?

В некоторых СУБД (например, PostgreSQL) оптимизатор может «развернуть» простой вложенный запрос в полусоединение (semi-join), которое эффективнее полного JOIN, если нужна только проверка существования, а не данные из связанной таблицы.

Как оптимизировать вложенные запросы: 7 практических советов

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

  1. Ограничивайте объём данных во вложенном запросе. Используйте ПЕРВЫЕ N, РАЗЛИЧНЫЕ или фильтры по датам, чтобы уменьшить количество строк, которые придётся обрабатывать.
  2. Избегайте корреляции, если она не нужна. Если вложенный запрос не зависит от полей внешнего, вынесите его в отдельную временную таблицу.
  3. Проверяйте план выполнения. В 1С:Предприятие это можно сделать через ОбъяснитьЗапрос() или в инструментах СУБД (например, SQL Server Management Studio). Ищите операции Nested Loops — они часто указывают на неэффективные вложенные запросы.
  4. Заменяйте В (подзапрос) на СУЩЕСТВУЕТ, если нужна только проверка существования.
  5. Используйте индексы. Убедитесь, что поля, по которым соединяются таблицы или фильтруются данные, проиндексированы в базе.
  6. Разбивайте сложные запросы. Если запрос содержит более 3 уровней вложенности, подумайте о рефакторинге с использованием временных таблиц.
  7. Тестируйте на реальных данных. Производительность может радикально отличаться на тестовых базах (100 строк) и боевых (10 млн строк).

Пример оптимизации: вместо

ВЫБРАТЬ

Клиенты.Наименование

ИЗ

Справочник.Клиенты КАК Клиенты

ГДЕ

Клиенты.Ссылка В (

ВЫБРАТЬ

Заказы.Клиент

ИЗ

Документ.ЗаказПокупателя КАК Заказы

ГДЕ

Заказы.Дата >= &НачалоГода

)

лучше написать:

ВЫБРАТЬ РАЗЛИЧНЫЕ

Заказы.Клиент КАК Ссылка

ПОМЕСТИТЬ ВТ_КлиентыСЗаказами

ИЗ

Документ.ЗаказПокупателя КАК Заказы

ГДЕ

Заказы.Дата >= &НачалоГода

;

////////////////////////////////////////////////

ВЫБРАТЬ

Клиенты.Наименование

ИЗ

Справочник.Клиенты КАК Клиенты

ГДЕ

Клиенты.Ссылка В (&ВТ_КлиентыСЗаказами)

Во втором варианте вложенный запрос выполняется один раз, а его результат сохраняется во временной таблице.

Можно ли заменить его на JOIN или временную таблицу?|Есть ли индексы на поля, используемые в условиях?|Будет ли вложенный запрос возвращать мало строк?|Нужна ли корреляция с внешним запросом?|Могу ли я ограничить данные во вложенном запросе (ПЕРВЫЕ, фильтры)?-->

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

Есть сценарии, где вложенные запросы не просто неэффективны, а гарантированно приведут к проблемам. Вот самые критичные случаи:

  1. Вложенные запросы в циклах (см. раздел об ошибках). Это самый верный способ «убить» производительность.
  2. Работа с большими таблицами регистров. Если вложенный запрос обращается к регистру накопления с миллионами записей, даже при наличии индексов это может быть медленно.
  3. Многократное выполнение одного и того же подзапроса. Например, если в отчёте один и тот же вложенный запрос используется в нескольких местах.
  4. Вложенные запросы с агрегатными функциями (например, СУММА, СРЕДНЕЕ) по большим выборкам. Такие операции блокируют оптимизацию.
  5. Коррелированные подзапросы с полнотекстовым поиском. Полнотекстовый поиск сам по себе ресурсоёмкий, а в сочетании с вложенностью становится катастрофой.

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

Пример «антипаттерна»:

ВЫБРАТЬ

Товары.Наименование,

(

ВЫБРАТЬ

Остатки.КоличествоОстаток

ИЗ

РегистрНакопления.ОстаткиТоваров КАК Остатки

ГДЕ

Остатки.Товар = Товары.Ссылка

И Остатки.Склад = &ТекущийСклад

) КАК Остаток

ИЗ

Справочник.Товары КАК Товары

Здесь для каждого товара выполняется отдельный запрос к регистру. Правильное решение — использовать виртуальную таблицу ОстаткиИОбороты:

ВЫБРАТЬ

Остатки.Товар КАК Ссылка,

Остатки.КоличествоОстаток КАК Остаток

ИЗ

РегистрНакопления.ОстаткиТоваров.ОстаткиИОбороты(&ТекущийСклад, ) КАК Остатки

💡

Если вы видите в плане запроса операцию Table Scan (полное сканирование таблицы) для вложенного запроса, это верный знак, что его нужно переписать.

Альтернативы вложенным запросам: что использовать вместо них

В большинстве случаев вложенные запросы можно заменить на более эффективные конструкции. Рассмотрим основные альтернативы.

1. Временные таблицы

Идеальны, когда:

  • 🔹 Нужно использовать один и тот же набор данных несколько раз.
  • 🔹 Вложенный запрос возвращает много строк.
  • 🔹 Нужно предварительно агрегировать данные.

Пример:

ВЫБРАТЬ

Заказы.Клиент КАК Клиент,

СУММА(Заказы.Сумма) КАК СуммаЗаказов

ИЗ

Документ.ЗаказПокупателя КАК Заказы

ГДЕ

Заказы.Дата >= &НачалоГода

СГРУППИРОВАТЬ ПО

Заказы.Клиент

ПОМЕСТИТЬ ВТ_СуммыЗаказов

;

////////////////////////////////////////////////

ВЫБРАТЬ

Клиенты.Наименование,

ВТ_СуммыЗаказов.СуммаЗаказов

ИЗ

Справочник.Клиенты КАК Клиенты

ЛЕВОЕ СОЕДИНЕНИЕ ВТ_СуммыЗаказов КАК ВТ_СуммыЗаказов

ПО Клиенты.Ссылка = ВТ_СуммыЗаказов.Клиент

2. Объединения (JOIN)

Подходят для:

  • 🔹 Связывания таблиц по ключам.
  • 🔹 Получения данных из нескольких источников в одной выборке.
  • 🔹 Замены коррелированных подзапросов.

Пример замены вложенного запроса на LEFT JOIN:

-- Вложенный запрос (медленно)

ВЫБРАТЬ

Клиенты.Наименование

ИЗ

Справочник.Клиенты КАК Клиенты

ГДЕ

НЕ Клиенты.Ссылка В (

ВЫБРАТЬ РАЗЛИЧНЫЕ Заказы.Клиент