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

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

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

Анализ проблемы: превращение левого соединения во внутреннее

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


ВЫБРАТЬ РАЗРЕШЕННЫЕ
    ТЗ.Ссылка КАК Ссылка,
    СчетНаОплатуПокупателюЗаявки.Ссылка КАК Счет
ПОМЕСТИТЬ Счета
ИЗ
    ТЗ КАК ТЗ
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.СчетНаОплатуПокупателю.Заявки КАК СчетНаОплатуПокупателюЗаявки
    ПО (ТЗ.Ссылка = СчетНаОплатуПокупателюЗаявки.Заявка)
ГДЕ
    СчетНаОплатуПокупателюЗаявки.Ссылка.Проведен

Проанализируем, как выполняется этот запрос в СУБД. Конструкция ЛЕВОЕ СОЕДИНЕНИЕ предполагает, что из левой таблицы (ТЗ) будут возвращены абсолютно все строки. Если в правой таблице (СчетНаОплатуПокупателю.Заявки) соответствий не найдено, то во всех полях правой таблицы подставляется значение NULL.

Однако далее в секции ГДЕ указано условие: СчетНаОплатуПокупателюЗаявки.Ссылка.Проведен. Когда для заявки нет счета на оплату, поле СчетНаОплатуПокупателюЗаявки.Ссылка принимает значение NULL. Любая логическая проверка или сравнение с NULL в языке запросов дает значение Ложь (или UNKNOWN, что фильтром ГДЕ трактуется как невыполнение условия). В результате строка, у которой нет счета, безвозвратно отсекается.

Таким образом, условие в секции ГДЕ, наложенное на реквизиты правой таблицы, автоматически превращает левое соединение во внутреннее. Если в цепочке связей дальше идет такое же соединение с документами поступления денег и фильтром ГДЕ ПоступлениеНаРасчетныйСчет.Ссылка.Проведен, то в результат попадут только те заявки, по которым одновременно есть и проведенный счет, и проведенная банковская выписка. Если хотя бы одного документа нет — запрос вернет пустоту.

Решение 1. Перенос условий фильтрации в секцию ПО

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

Посмотрим на исправленный фрагмент запроса:


ВЫБРАТЬ РАЗРЕШЕННЫЕ
    ТЗ.Ссылка КАК Ссылка,
    СчетНаОплатуПокупателюЗаявки.Ссылка КАК Счет,
    ТЗ.Возвращен КАК Возвращен
ПОМЕСТИТЬ Счета
ИЗ
    ТЗ КАК ТЗ
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.СчетНаОплатуПокупателю.Заявки КАК СчетНаОплатуПокупателюЗаявки
    ПО (ТЗ.Ссылка = СчетНаОплатуПокупателюЗаявки.Заявка)
        И (СчетНаОплатуПокупателюЗаявки.Ссылка.Проведен = ИСТИНА)

В этом случае платформа сначала отфильтрует правую таблицу, оставив только проведенные счета, а затем соединит полученный результат с таблицей ТЗ. Если проведенного счета нет, заявка все равно останется в результате, а поля счета заполнятся значениями NULL.

Аналогично поступим и на следующем шаге с табличной частью платежей:


ВЫБРАТЬ РАЗРЕШЕННЫЕ
    Счета.Ссылка КАК Счет,
    Счета.Заявка КАК Заявка,
    ЕСТЬNULL(Поступление.СуммаВзаиморасчетов, 0) КАК СуммаВзаиморасчетов
ИЗ
    Счета КАК Счета
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеНаРасчетныйСчет.РасшифровкаПлатежа КАК Поступление
    ПО (Счета.Счет = Поступление.СчетНаОплату)
        И (Поступление.Ссылка.Проведен = ИСТИНА)

Решение 2. Оптимизация работы в событии ПриПолученииДанных

Событие ПриПолученииДанных в формах списков вызывается непрерывно при прокрутке строк, изменении фокуса и обновлении данных. Неоптимальный код в этом обработчике приводит к существенным зависаниям интерфейса («фризам»). Реализуем комплекс мер по повышению производительности:

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

Посмотрим на итоговый оптимизированный пример кода процедуры:


Процедура ДокументСписокПриПолученииДанных(Элемент, ОформленияСтрок)
    
    МассивЗаявок = Новый Массив;
    Для Каждого Оформление Из ОформленияСтрок Цикл
        МассивЗаявок.Добавить(Оформление.ДанныеСтроки.Ссылка);
    КонецЦикла;
    
    Если МассивЗаявок.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    МенеджерВТ = Новый МенеджерВременныхТаблиц;
    Запрос = Новый Запрос;
    Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
    Запрос.УстановитьПараметр("МассивЗаявок", МассивЗаявок);
    
    Запрос.Текст = 
    "ВЫБРАТЬ РАЗРЕШЕННЫЕ
    |    СчетЗаявки.Заявка КАК Заявка,
    |    СчетЗаявки.Ссылка КАК СчетНаОплату
    |ПОМЕСТИТЬ ВТ_Счета
    |ИЗ
    |    Документ.СчетНаОплатуПокупателю.Заявки КАК СчетЗаявки
    |ГДЕ
    |    СчетЗаявки.Заявка В(&МассивЗаявок)
    |    И СчетЗаявки.Ссылка.Проведен = ИСТИНА
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ РАЗРЕШЕННЫЕ
    |    ВТ_Счета.Заявка КАК Заявка,
    |    СУММА(ЕСТЬNULL(Оплата.СуммаВзаиморасчетов, 0)) КАК СуммаОплаты
    |ИЗ
    |    ВТ_Счета КАК ВТ_Счета
    |    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеНаРасчетныйСчет.РасшифровкаПлатежа КАК Оплата
    |        ПО (ВТ_Счета.СчетНаОплату = Оплата.СчетНаОплату)
    |        И (Оплата.Ссылка.Проведен = ИСТИНА)
    |СГРУППИРОВАТЬ ПО
    |    ВТ_Счета.Заявка";
    
    Результат = Запрос.Выполнить().Выгрузить();
    МенеджерВТ.Закрыть();
    
    // Быстрое сопоставление через Соответствие
    СоответствиеОплат = Новый Соответствие;
    Для Каждого Стр Из Результат Цикл
        СоответствиеОплат.Вставить(Стр.Заявка, Стр.СуммаОплаты);
    КонецЦикла;
    
    Для Каждого Оформление Из ОформленияСтрок Цикл
        Сумма = СоответствиеОплат[Оформление.ДанныеСтроки.Ссылка];
        Если Сумма <> Неопределено Тогда
            // Устанавливаем значение в добавленную колонку списка
            Оформление.Ячейки.СуммаОплаты.Значение = Сумма;
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

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

← На главную