При написании сложных запросов в системе «1С:Предприятие 8» разработчики часто сталкиваются с необходимостью динамического вычисления данных на основе условий. Первое интуитивное решение, которое приходит в голову программистам с опытом работы в классических СУБД (например, MS SQL Server или PostgreSQL) — это вставить вложенный подзапрос напрямую внутрь конструкции ВЫБОР. Однако при попытке сделать это в 1С платформа выдает ошибку синтаксиса.
В рамках данной статьи мы подробно разберем, почему стандартный подход со скалярными подзапросами не работает в языке запросов 1С, в каких исключительных случаях вложенные запросы все-таки можно использовать внутри ВЫБОР, и как правильно решать подобные прикладные задачи с использованием временных таблиц и объединений.
В классическом SQL существует понятие скалярного подзапроса (scalar subquery) — это запрос в списке выбора (секции SELECT), который возвращает ровно одну строку и одну колонку. СУБД умеет обрабатывать такие конструкции «на лету» для каждой строки выборки.
Язык запросов 1С является надстройкой и абстракцией над SQL, созданной для обеспечения кроссплатформенности и объектного сопоставления. Платформа 1С не поддерживает скалярные подзапросы в списке выбора и в условных конструкциях. Попытка написать следующий код приведет к синтаксической ошибке на этапе компиляции запроса:
// ВНИМАНИЕ: Данный код вызовет синтаксическую ошибку!
ВЫБРАТЬ
Статьи.Ссылка КАК Статья,
ВЫБОР
КОГДА Статьи.Ссылка = ЗНАЧЕНИЕ(Справочник.СтатьиДвиженияДенежныхСредств.ВыдачаДенежныхСредствКассеККМ)
ТОГДА (ВЫБРАТЬ СУММА(Обороты.СуммаОборот) ИЗ РегистрНакопления.ДвиженияДенежныхСредств.Обороты КАК Обороты)
ИНАЧЕ 0
КОНЕЦ КАК Сумма
ИЗ
Справочник.СтатьиДвиженияДенежныхСредств КАК Статьи
В ячейку результирующей таблицы запроса 1С невозможно поместить другую таблицу или результат независимой выборки. Любые данные, которые мы хотим вывести на экран или использовать для дальнейших расчетов, должны быть предварительно соединены или объединены на уровне табличных источников.
Несмотря на жесткие ограничения, в языке запросов 1С существует единственное легитимное место для использования вложенных запросов внутри оператора ВЫБОР — это секция условия КОГДА. Но использовать их там можно только в связке с оператором проверки вхождения В (ВЫБРАТЬ...).
Рассмотрим два наиболее частых сценария использования этого механизма.
Сценарий А. Проверка вхождения элемента во временную таблицу или выборку
Мы можем проверить, входит ли анализируемое поле текущей строки в результат другого запроса. При этом вложенный запрос обязан возвращать строго одну колонку.
ВЫБРАТЬ
СправочникНоменклатура.Ссылка КАК Товар,
ВЫБОР
КОГДА СправочникНоменклатура.Ссылка В (
ВЫБРАТЬ
ВТ_АкционныеТовары.Номенклатура
ИЗ
ВТ_АкционныеТовары КАК ВТ_АкционныеТовары
) ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ КАК ЭтоАкционныйТовар
ИЗ
Справочник.Номенклатура КАК СправочникНоменклатура
Сценарий Б. Эмуляция отсутствующего оператора EXISTS
В языке запросов 1С нет прямого аналога SQL-оператора EXISTS, который проверяет наличие связанных записей в подчиненных таблицах. Однако мы можем элегантно сэмулировать его поведение с помощью связки 1 В (ВЫБРАТЬ ПЕРВЫЕ 1 1...) в условии КОГДА:
ВЫБРАТЬ
ДокументРеализация.Ссылка КАК ДокументСсылка,
ДокументРеализация.Контрагент КАК Контрагент,
ВЫБОР
КОГДА 1 В (
ВЫБРАТЬ ПЕРВЫЕ 1
1
ИЗ
Документ.СчетФактураВыданный КАК СФ
ГДЕ
СФ.ДокументОснование = ДокументРеализация.Ссылка
) ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ КАК ЕстьСчетФактура
ИЗ
Документ.РеализацияТоваровУслуг КАК ДокументРеализация
Этот прием позволяет быстро проверить факт наличия подчиненного документа (в данном случае — выписанного счета-фактуры) без выполнения тяжелого левого соединения всей таблицы документов.
Проанализируем конкретную прикладную задачу, которую пытался решить автор обсуждения. Ему необходимо при расчете оборотов по статьям движения денежных средств (ДДС) сделать следующее:
ВыдачаДенежныхСредствКассеККМ, то вместо ее собственного нулевого или фактического оборота необходимо вывести сумму оборотов по нескольким другим конкретным статьям.Для реализации такой логики существуют два классических, надежных и производительных подхода, полностью соответствующих стандартам разработки фирмы «1С».
Это наиболее структурированный и понятный способ. Вместо попыток рассчитать сумму «на лету» внутри условия, мы выносим расчет суммы по «дополнительным» статьям в отдельный предварительный шаг — во временную таблицу.
Разберем это решение по шагам:
ВЫБРАТЬ мы можем использовать простую условную конструкцию, подставляя значение из соединенной таблицы при совпадении целевой статьи.Посмотрим на пример реализации данного алгоритма:
// ШАГ 1: Собираем сумму по дополнительным статьям в разрезе торговых точек
ВЫБРАТЬ
ДвиженияОборотыДоп.БанковскийСчетКасса.ТорговаяТочка КАК ТорговаяТочка,
СУММА(ДвиженияОборотыДоп.СуммаОборот) КАК СуммаОборотДоп
ПОМЕСТИТЬ ВТ_СуммыДополнительныхСтатей
ИЗ
РегистрНакопления.ДвиженияДенежныхСредств.Обороты(
&НачалоПериода,
&КонецПериода,
Авто,
СтатьяДвиженияДенежныхСредств В (&МассивДопСтатей)
) КАК ДвиженияОборотыДоп
СГРУППИРОВАТЬ ПО
ДвиженияОборотыДоп.БанковскийСчетКасса.ТорговаяТочка
ИНДЕКСИРОВАТЬ ПО
ТорговаяТочка
;
// ШАГ 2: Основной запрос пакета
ВЫБРАТЬ
СтатьиДДС.Ссылка КАК СтатьяДвиженияДенежныхСредств,
ДвиженияОборотыОсновной.БанковскийСчетКасса.ТорговаяТочка КАК БанковскийСчетКассаТорговаяТочка,
ВЫБОР
КОГДА СтатьиДДС.Ссылка = ЗНАЧЕНИЕ(Справочник.СтатьиДвиженияДенежныхСредств.ВыдачаДенежныхСредствКассеККМ)
ТОГДА ЕСТЬNULL(ВТ_Доп.СуммаОборотДоп, 0)
ИНАЧЕ ЕСТЬNULL(ДвиженияОборотыОсновной.СуммаОборот, 0)
КОНЕЦ КАК СуммаОборот
ИЗ
Справочник.СтатьиДвиженияДенежныхСредств КАК СтатьиДДС
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвиженияДенежныхСредств.Обороты(&НачалоПериода, &КонецПериода, Авто, ) КАК ДвиженияОборотыОсновной
ПО СтатьиДДС.Ссылка = ДвиженияОборотыОсновной.СтатьяДвиженияДенежныхСредств
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_СуммыДополнительныхСтатей КАК ВТ_Доп
ПО ДвиженияОборотыОсновной.БанковскийСчетКасса.ТорговаяТочка = ВТ_Доп.ТорговаяТочка
Этот прием часто оказывается более производительным, чем левое соединение, так как он избавляет СУБД от необходимости строить сложные планы соединений для больших объемов данных. Мы разбиваем выборку на две независимые ветки, объединяем их результаты и «схлопываем» при помощи итоговой группировки.
Рассмотрим данный подход подробнее:
ВыдачаДенежныхСредствКассеККМ мы принудительно возвращаем оборот, равный 0, чтобы ее собственные движения не задвоились.СУММА() автоматически сложит нулевое значение целевой статьи и подтянутые из второй ветки обороты дополнительных статей.Посмотрим на практическую реализацию этого метода:
ВЫБРАТЬ
ВыборкаОборотов.СтатьяДДС КАК СтатьяДвиженияДенежныхСредств,
ВыборкаОборотов.ТорговаяТочка КАК БанковскийСчетКассаТорговаяТочка,
СУММА(ВыборкаОборотов.Сумма) КАК СуммаОборот
ИЗ
(
// Часть 1: Выбираем все стандартные обороты (для целевой статьи принудительно ставим 0)
ВЫБРАТЬ
ОборотыОсновной.СтатьяДвиженияДенежныхСредств КАК СтатьяДДС,
ОборотыОсновной.БанковскийСчетКасса.ТорговаяТочка КАК ТорговаяТочка,
ВЫБОР
КОГДА ОборотыОсновной.СтатьяДвиженияДенежныхСредств = ЗНАЧЕНИЕ(Справочник.СтатьиДвиженияДенежныхСредств.ВыдачаДенежныхСредствКассеККМ)
ТОГДА 0
ИНАЧЕ ОборотыОсновной.СуммаОборот
КОНЕЦ КАК Сумма
ИЗ
РегистрНакопления.ДвиженияДенежныхСредств.Обороты(&НачалоПериода, &КонецПериода, Авто, ) КАК ОборотыОсновной
ОБЪЕДИНИТЬ ВСЕ
// Часть 2: Выбираем обороты по доп. статьям и принудительно сопоставляем их целевой статье
ВЫБРАТЬ
ЗНАЧЕНИЕ(Справочник.СтатьиДвиженияДенежныхСредств.ВыдачаДенежныхСредствКассеККМ) КАК СтатьяДДС,
ОборотыДополнительные.БанковскийСчетКасса.ТорговаяТочка КАК ТорговаяТочка,
ОборотыДополнительные.СуммаОборот КАК Сумма
ИЗ
РегистрНакопления.ДвиженияДенежныхСредств.Обороты(
&НачалоПериода,
&КонецПериода,
Авто,
СтатьяДвиженияДенежныхСредств В (&МассивДопСтатей)
) КАК ОборотыДополнительные
) КАК ВыборкаОборотов
СГРУППИРОВАТЬ ПО
ВыборкаОборотов.СтатьяДДС,
ВыборкаОборотов.ТорговаяТочка
При работе со сложными выборками и агрегацией данных крайне важно соблюдать стандарты разработки «1С», чтобы ваши отчеты не вызывали перегрузки сервера СУБД и работали быстро даже на больших базах данных:
ГДЕ итогового запроса. Все условия отбора (например, СтатьяДвиженияДенежныхСредств В (&МассивДопСтатей)) должны передаваться строго в параметры виртуальной таблицы Обороты. Это позволяет СУБД отсечь ненужные записи на самом раннем этапе.ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ...). Оптимизатор СУБД часто не может построить эффективный план для таких запросов. Всегда переносите вложенный запрос во временную таблицу (ПОМЕСТИТЬ ВТ_...), индексируйте её и только потом соединяйте.ВЫБОР старайтесь сравнивать простые типы или ссылки. Избегайте сравнения строк неограниченной длины и сложных составных типов данных.Таким образом, правильное структурирование запросов с использованием механизмов временных таблиц и объединений позволяет полностью решить любые задачи условной выборки данных без применения неэффективных или синтаксически некорректных конструкций.