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