Почему ЛЕВОЕ СОЕДИНЕНИЕ во вложенном запросе 1С возвращает NULL и как это исправить

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке сложных отчетов или выборок в системе 1С:Предприятие программисты часто сталкиваются с ситуацией, когда левое соединение с вложенным запросом возвращает NULL вместо ожидаемого дефолтного значения (например, пустой строки, нуля или единицы). В этой статье мы подробно разберем, почему возникает такая проблема на уровне СУБД, рассмотрим типичные ошибки при попытке обойти её внутри вложенного запроса и покажем, как правильно оптимизировать код в соответствии со стандартами разработки 1С.

В чем суть проблемы? Анализируем исходную ситуацию

Рассмотрим практический сценарий. У нас есть основной документ (например, бит_стр_ПриходныйОрдер), к которому мы хотим присоединить информацию о наличии связанных файлов из справочника дополнительной информации (бит_ХранилищеДополнительнойИнформации). Мы хотим получить флаг ФайлыЕсть, который принимает значение:

Для реализации этой логики разработчик пишет вложенный запрос с группировкой по объектам и конструкцией ВЫБОР:


ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
    МАКСИМУМ(ВЫБОР
            КОГДА (ВЫРАЗИТЬ(бит_ХранилищеДополнительнойИнформации.ИмяФайла КАК СТРОКА(255))) <> ""
                ТОГДА 0
            ИНАЧЕ 1
        КОНЕЦ) КАК ФайлыЕсть,
    бит_ХранилищеДополнительнойИнформации.Объект КАК Объект
ИЗ
    Справочник.бит_ХранилищеДополнительнойИнформации КАК бит_ХранилищеДополнительнойИнформации

СГРУППИРОВАТЬ ПО
    бит_ХранилищеДополнительнойИнформации.Объект) КАК ВложенныйЗапрос
ПО Документбит_стр_ПриходныйОрдер.Ссылка = ВложенныйЗапрос.Объект

При выполнении такого запроса для документов, у которых вообще нет записей в справочнике доп. информации, поле ВложенныйЗапрос.ФайлыЕсть возвращает значение NULL, хотя разработчик ожидал получить 1 (согласно ветке ИНАЧЕ 1). Выясним причину этого поведения.

Почему конструкция ВЫБОР внутри вложенного запроса возвращает NULL?

Чтобы понять логику работы СУБД, разделим выполнение запроса на два последовательных этапа:

  1. Выполнение вложенного запроса. СУБД обращается к физической таблице справочника бит_ХранилищеДополнительнойИнформации. Если для конкретного документа в этой таблице вообще нет ни одной записи, то в результирующей выборке вложенного запроса строки с ссылкой на данный документ просто не будет. Соответственно, условие ВЫБОР для этой строки даже не начинает вычисляться.
  2. Выполнение ЛЕВОГО СОЕДИНЕНИЯ. СУБД берет таблицу документов и пытается сопоставить её со строками, которые вернул вложенный запрос. Для тех документов, по которым вложенный запрос не вернул строк, СУБД принудительно заполняет все поля правой таблицы значением NULL. Это базовый стандарт работы механизма LEFT JOIN в реляционных базах данных.

Попытки разработчика написать внутри вложенного запроса условия вида КОГДА ИмяФайла ЕСТЬ NULL ТОГДА 1 или обернуть поле в функцию ЕСТЬNULL на уровне вложенного запроса не принесут результата. Проверка на NULL внутри подзапроса работает только для тех записей, которые физически существуют в базе данных, но содержат пустое значение в поле. Для полностью отсутствующих записей проверка должна выполняться на самом верхнем уровне.

Решение проблемы: Правильное использование ЕСТЬNULL

Самое простое и быстрое исправление логической ошибки заключается в переносе функции ЕСТЬNULL в секцию ВЫБРАТЬ основного (внешнего) запроса. Мы должны обрабатывать результат соединения уже после того, как СУБД попыталась сопоставить таблицы.

Рассмотрим, как изменится поле выборки в основном запросе:


// Вместо обращения напрямую: ВложенныйЗапрос.ФайлыЕсть
// Мы оборачиваем поле в функцию ЕСТЬNULL и указываем дефолтное значение 1
ЕСТЬNULL(ВложенныйЗапрос.ФайлыЕсть, 1) КАК ФайлыЕсть

Теперь логика будет отрабатывать корректно:

Оптимизация запроса по стандартам 1С: Переход на Временные Таблицы (ВТ)

Хотя исправление на верхнем уровне решает проблему, использование вложенных запросов в соединениях крайне не рекомендуется стандартами разработки фирмы "1С" (особенно для систем, работающих под высокой нагрузкой).

Почему вложенные запросы в соединениях — это плохо?
При наличии вложенного запроса оптимизатор СУБД (например, MS SQL Server или PostgreSQL) не может построить эффективный план выполнения запроса, так как не имеет точной статистики по количеству строк, которые вернет подзапрос. Это часто приводит к неверному выбору метода соединения таблиц (например, выбору ресурсоемкого *Nested Loops* вместо *Hash Match*), зависаниям запроса и блокировкам таблиц.

Проанализируем оптимальный вариант переписывания запроса с использованием механизмов временных таблиц и индексации. Мы также решим сопутствующую проблему — исключим соединение по составному типу данных (так как поле Объект обычно имеет составной тип), предварительно отфильтровав его.

Разберем оптимальный код по шагам:


// ШАГ 1: Помещаем данные о файлах во временную таблицу
// Обязательно фильтруем по типу объекта, чтобы исключить лишние типы данных и неявные преобразования типов
ВЫБРАТЬ
    бит_ХранилищеДополнительнойИнформации.Объект КАК Объект,
    МАКСИМУМ(ВЫБОР
            КОГДА ЕСТЬNULL(ВЫРАЗИТЬ(бит_ХранилищеДополнительнойИнформации.ИмяФайла КАК СТРОКА(255)), "") <> ""
                ТОГДА 0
            ИНАЧЕ 1
        КОНЕЦ) КАК ФайлыЕсть
ПОМЕСТИТЬ ВТ_НаличиеФайлов
ИЗ
    Справочник.бит_ХранилищеДополнительнойИнформации КАК бит_ХранилищеДополнительнойИнформации
ГДЕ
    ТИПЗНАЧЕНИЯ(бит_ХранилищеДополнительнойИнформации.Объект) = ТИП(Документ.бит_стр_ПриходныйОрдер)

СГРУППИРОВАТЬ ПО
    бит_ХранилищеДополнительнойИнформации.Объект

// Индексируем поле связи во временной таблице для ускорения последующего соединения
ИНДЕКСИРОВАТЬ ПО
    Объект
;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 2: Основной запрос выборки данных
ВЫБРАТЬ
    ДокументПриходныйОрдер.Ссылка КАК Ссылка,
    ДокументПриходныйОрдер.Номер КАК Номер,
    ДокументПриходныйОрдер.Дата КАК Дата,
    ДокументПриходныйОрдер.Организация КАК Организация,
    
    // Получаем данные по входящим документам ТОРГ-12 и Счет-Фактурам
    ВходящиеДокументыТН.НомерДокумента КАК НомерТН,
    ВходящиеДокументыТН.ДатаДокумента КАК ДатаТН,
    ВходящиеДокументыСчФ.ДатаДокумента КАК ДатаСчФ,
    ВходящиеДокументыСчФ.НомерДокумента КАК НомерСчФ,
    
    // Применяем ЕСТЬNULL к проиндексированной временной таблице
    ЕСТЬNULL(ВТ_НаличиеФайлов.ФайлыЕсть, 1) КАК ФайлыЕсть
ИЗ
    Документ.бит_стр_ПриходныйОрдер КАК ДокументПриходныйОрдер
    
    // Соединение для получения ТОРГ-12
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.бит_стр_ВходящиеДокументы КАК ВходящиеДокументыТН
        ПО (ВходящиеДокументыТН.ДокументОснование = ДокументПриходныйОрдер.Ссылка)
        И (ВходящиеДокументыТН.ВидДокумента = ЗНАЧЕНИЕ(Справочник.бит_стр_ВидыДокументов.ТОРГ12))
        
    // Соединение для получения Счет-Фактуры
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.бит_стр_ВходящиеДокументы КАК ВходящиеДокументыСчФ
        ПО (ВходящиеДокументыСчФ.ДокументОснование = ДокументПриходныйОрдер.Ссылка)
        И (ВходящиеДокументыСчФ.ВидДокумента = ЗНАЧЕНИЕ(Справочник.бит_стр_ВидыДокументов.СчетФактура))
        
    // Эффективное левое соединение с временной таблицей
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_НаличиеФайлов КАК ВТ_НаличиеФайлов
        ПО (ВТ_НаличиеФайлов.Объект = ДокументПриходныйОрдер.Ссылка)

Преимущества использования ВТ в данном сценарии

Переписав запрос с использованием временных таблиц, мы получаем ряд преимуществ:

Таким образом, мы не только исправили логическую ошибку получения значения NULL, но и привели запрос к стандартам производительности 1С, обеспечив его быструю и стабильную работу даже на крупных базах данных с миллионами записей.

← На главную