Почему левое соединение с вложенным запросом возвращает NULL и как получить одну запись из подчиненной таблицы в 1С?

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

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

Анализ проблемного запроса

Рассмотрим код, который часто пытаются использовать для получения первого попавшегося образования сотрудника:


ВЫБРАТЬ
    Сотрудники.Код КАК Код,
    Сотрудники.Наименование КАК Наименование,
    ВложенныйЗапрос.ВидОбразования КАК ВидОбразования,
    ВложенныйЗапрос.УчебноеЗаведение КАК УчебноеЗаведение
ИЗ
    Справочник.Сотрудники КАК Сотрудники
        ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ПЕРВЫЕ 1
            ОбразованиеФизическихЛиц.Владелец КАК Владелец,
            ОбразованиеФизическихЛиц.ВидОбразования КАК ВидОбразования,
            ОбразованиеФизическихЛиц.УчебноеЗаведение КАК УчебноеЗаведение,
            ОбразованиеФизическихЛиц.Ссылка КАК Ссылка
        ИЗ
            Справочник.ОбразованиеФизическихЛиц КАК ОбразованиеФизическихЛиц
        УПОРЯДОЧИТЬ ПО
            Ссылка) КАК ВложенныйЗапрос
        ПО Сотрудники.ФизическоеЛицо.Ссылка = ВложенныйЗапрос.Владелец.Ссылка
ГДЕ
    Сотрудники.Код = &Код

На первый взгляд логика кажется верной: мы просим платформу взять таблицу образования, выбрать первую запись и присоединить ее к сотруднику. Однако на практике данные возвращаются только для одного физлица, а для остальных поля ВидОбразования и УчебноеЗаведение заполняются значением NULL.

Выясним причину: как 1С выполняет вложенные запросы

Проблема кроется в архитектурных особенностях языка запросов 1С. Вложенный запрос, который находится внутри конструкции ЛЕВОЕ СОЕДИНЕНИЕ, выполняется платформой как отдельный, независимый блок до самого соединения.

Когда мы пишем ВЫБРАТЬ ПЕРВЫЕ 1 во вложенном запросе, система просто берет самую первую попавшуюся запись из всей таблицы ОбразованиеФизическихЛиц (одну единственную запись на всю базу данных). Затем система пытается присоединить эту единственную строку к таблице Сотрудники. В итоге данные присоединятся только к тому сотруднику, чье физлицо случайно совпало с владельцем в этой единственной строке. Для всех остальных платформа справедливо вернет NULL, так как других записей во вложенном запросе просто нет.

В отличие от некоторых диалектов чистого SQL (где существует конструкция OUTER APPLY, позволяющая выполнять подзапрос заново для каждой строки основной таблицы), язык запросов 1С не поддерживает передачу ссылки из основной таблицы во вложенный запрос в секции ИЗ.

Способ 1: Решение через временные таблицы и группировку

Разберем подробнее оптимальный алгоритм действий, если данные хранятся в подчиненном справочнике. Чтобы получить ровно одну запись на каждого сотрудника, необходимо сгруппировать данные и найти уникальный идентификатор (ссылку) нужной строки.

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

Посмотрим на пример правильного кода:


// Шаг 1: Получаем одну (максимальную) ссылку на образование для каждого физлица
ВЫБРАТЬ
    Образование.Владелец КАК ФизическоеЛицо,
    МАКСИМУМ(Образование.Ссылка) КАК СсылкаНаОбразование
ПОМЕСТИТЬ ВТ_ПоследнееОбразование
ИЗ
    Справочник.ОбразованиеФизическихЛиц КАК Образование
СГРУППИРОВАТЬ ПО
    Образование.Владелец
;

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

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

Способ 2: Архитектурно правильное решение (Регистры сведений)

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

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


ВЫБРАТЬ
    Сотрудники.Код КАК Код,
    Сотрудники.Наименование КАК Наименование,
    ОбразованиеСрезПоследних.ВидОбразования КАК ВидОбразования,
    ОбразованиеСрезПоследних.УчебноеЗаведение КАК УчебноеЗаведение
ИЗ
    Справочник.Сотрудники КАК Сотрудники
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ОбразованиеФизическихЛиц.СрезПоследних КАК ОбразованиеСрезПоследних
        ПО Сотрудники.ФизическоеЛицо = ОбразованиеСрезПоследних.ФизическоеЛицо
ГДЕ
    Сотрудники.Код = &Код

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

← На главную