Почему Левое соединение в запросе 1С работает как Внутреннее и как это исправить?

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

При написании запросов в 1С начинающие (а иногда и опытные) разработчики сталкиваются с ситуацией, когда ЛЕВОЕ СОЕДИНЕНИЕ неожиданно обрезает данные, работая идентично ВНУТРЕННЕМУ СОЕДИНЕНИЮ. Обычно это происходит при попытке объединить основную таблицу (например, остатки бонусов) с дополнительной (контактная информация), когда по некоторым записям во второй таблице данных нет.

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

Причина проблемы: Влияние секции ГДЕ на результат соединения

Рассмотрим ситуацию, описанную в теме. Автор хочет получить все остатки бонусов и телефон владельца. Если телефона нет, запись об остатке все равно должна попасть в результат с пустым значением телефона. Однако, добавив условие ГДЕ КонтактнаяИнформация.Тип = &Тип, автор обнаружил, что записи без телефонов исчезли.

Проанализируем причину: когда мы используем ЛЕВОЕ СОЕДИНЕНИЕ, для объектов, у которых нет контактной информации, все поля таблицы КонтактнаяИнформация принимают значение NULL. Блок ГДЕ выполняется уже после того, как таблицы были соединены. Когда система проверяет условие NULL = &Тип, результат сравнения всегда будет ложным (или "неизвестным"). В итоге все строки, где контактной информации не было, отсекаются этим фильтром. Таким образом, ваше "левое" соединение фактически превращается во "внутреннее".

Решение 1: Перенос условия в секцию ПО

Самый простой и правильный способ решить эту проблему — перенести фильтрацию правой таблицы непосредственно в условие соединения ПО. В этом случае фильтр применится в момент соединения таблиц, и записи из левой таблицы не будут отсечены общим фильтром запроса.

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


Запрос.Текст = "ВЫБРАТЬ
|   ПодарочныеБонусыОстатки.ВладелецДисконтнойКарты КАК Владелец,
|   ПодарочныеБонусыОстатки.ДисконтнаяКарта КАК Карта,
|   ПодарочныеБонусыОстатки.СуммаПодарочногоБонусаОстаток КАК Остаток,
|   КонтактнаяИнформация.Поле3 КАК Телефон
|ИЗ
|   РегистрНакопления.ПодарочныеБонусы.Остатки КАК ПодарочныеБонусыОстатки
|   ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК КонтактнаяИнформация
|   ПО КонтактнаяИнформация.Объект = ПодарочныеБонусыОстатки.ВладелецДисконтнойКарты
|   И КонтактнаяИнформация.Тип = &Тип";

В данном случае условие КонтактнаяИнформация.Тип = &Тип ограничивает только правую таблицу. Если соответствие не найдено (тип не совпадает или записи нет вовсе), левая таблица сохранит свою строку, а в поле Телефон будет выведен NULL.

Решение 2: Обработка NULL в секции ГДЕ

Если по каким-то причинам вам необходимо оставить условие в блоке ГДЕ, придется явно разрешить значения NULL. Однако этот метод менее предпочтителен, так как он может быть менее производительным и сложнее читается.


ГДЕ
    (КонтактнаяИнформация.Тип = &Тип ИЛИ КонтактнаяИнформация.Тип ЕСТЬ NULL)

Это заставит систему пропустить записи, которые не подошли под тип, но оставить те, по которым данных в правой таблице вообще не нашлось.

Оптимизация запроса: использование временных таблиц

Проанализируем ситуацию глубже. В запросе автора напрямую соединяются виртуальная таблица Остатки и физическая таблица регистра сведений. В 1С это считается плохой практикой (антипаттерном) по нескольким причинам:

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

Разберем, как сделать запрос максимально профессиональным с использованием временных таблиц:


Запрос.Текст = 
"ВЫБРАТЬ
|   Остатки.ВладелецДисконтнойКарты КАК Владелец,
|   Остатки.ДисконтнаяКарта КАК Карта,
|   Остатки.СуммаПодарочногоБонусаОстаток КАК Остаток
|ПОМЕСТИТЬ ВТ_Остатки
|ИЗ
|   РегистрНакопления.ПодарочныеБонусы.Остатки КАК Остатки
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|   КИ.Объект КАК Объект,
|   МАКСИМУМ(КИ.Представление) КАК Телефон
|ПОМЕСТИТЬ ВТ_Телефоны
|ИЗ
|   РегистрСведений.КонтактнаяИнформация КАК КИ
|ГДЕ
|   КИ.Тип = &Тип
|СГРУППИРОВАТЬ ПО
|   КИ.Объект
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|   ВТ_Остатки.Владелец,
|   ВТ_Остатки.Карта,
|   ВТ_Остатки.Остаток,
|   ЕСТЬNULL(ВТ_Телефоны.Телефон, ""---"") КАК Телефон
|ИЗ
|   ВТ_Остатки КАК ВТ_Остатки
|   ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Телефоны КАК ВТ_Телефоны
|   ПО ВТ_Остатки.Владелец = ВТ_Телефоны.Объект";

Что мы улучшили в этом варианте:

Важный совет: При работе с регистром КонтактнаяИнформация в типовых конфигурациях (УТ 11, ERP, БП 3), поле Объект имеет составной тип. Для ускорения поиска в условии соединения рекомендуется использовать функцию ВЫРАЗИТЬ(КИ.Объект КАК Справочник.Контрагенты) или тот тип, который соответствует вашему владельцу карты. Это позволит базе данных обращаться к конкретной таблице, а не перебирать все возможные связи.

← На главную