Как в запросе 1С связать две версии табличной части документа, если одна из них пустая?

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

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

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

Анализ причин: почему итоговый запрос возвращает пустой результат

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

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

Вариант 1. Исправление левого соединения и правильная обработка NULL

Если нам гарантировано требуется вывести все актуальные строки текущего документа и показать дельту по отношению к предыдущей версии, ведущей таблицей обязательно должна выступать вт_ТекущаяВерсия.

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


ВЫБРАТЬ
    вт_ТекущаяВерсия.СтраховойРиск КАК СтраховойРиск,
    вт_ТекущаяВерсия.Ссылка КАК ТекущаяВерсия,
    вт_ПредыдущаяВерсия.Ссылка КАК ПредыдущаяВерсия,
    ЕСТЬNULL(вт_ТекущаяВерсия.СтраховаяСумма, 0) - ЕСТЬNULL(вт_ПредыдущаяВерсия.СтраховаяСумма, 0) КАК РазницаСуммы
ПОМЕСТИТЬ вт_ИтоговаяСтраховаяСумма
ИЗ
    вт_ТекущаяВерсия КАК вт_ТекущаяВерсия
    ЛЕВОЕ СОЕДИНЕНИЕ вт_ПредыдущаяВерсия КАК вт_ПредыдущаяВерсия
        ПО вт_ТекущаяВерсия.СтраховойРиск = вт_ПредыдущаяВерсия.СтраховойРиск
ГДЕ
    ЕСТЬNULL(вт_ТекущаяВерсия.СтраховаяСумма, 0) <> ЕСТЬNULL(вт_ПредыдущаяВерсия.СтраховаяСумма, 0)

Разберем важные нюансы этого варианта:

  1. Мы поместили вт_ТекущаяВерсия в секцию ИЗ первой, сделав ее главной. Даже если в вт_ПредыдущаяВерсия ноль строк, все риски текущего документа останутся в выборке.
  2. Все поля числовых сумм в выражениях разницы и в секции ГДЕ обернуты в функцию ЕСТЬNULL(..., 0). Теперь, если предыдущая версия отсутствует, число сравнивается с нулем, условие неравенства отрабатывает как Истина, и строка успешно попадает в результат.

Вариант 2. Схема через «ОБЪЕДИНИТЬ ВСЕ» с группировкой (универсальный подход)

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

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


ВЫБРАТЬ
    ТекущиеРиски.СтраховойРиск КАК СтраховойРиск,
    ТекущиеРиски.СтраховаяСумма КАК СуммаТекущая,
    0 КАК СуммаПредыдущая
ПОМЕСТИТЬ вт_ВсеДанные
ИЗ
    Документ.ДоговорСтрахования.РискиПоЗастрахованным КАК ТекущиеРиски
ГДЕ
    ТекущиеРиски.Ссылка = &ТекущаяВерсия

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ПредыдущиеРиски.СтраховойРиск,
    0,
    ПредыдущиеРиски.СтраховаяСумма
ИЗ
    Документ.ДоговорСтрахования.РискиПоЗастрахованным КАК ПредыдущиеРиски
ГДЕ
    ПредыдущиеРиски.Ссылка = &ПредыдущаяВерсия
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    вт_ВсеДанные.СтраховойРиск КАК СтраховойРиск,
    СУММА(вт_ВсеДанные.СуммаТекущая) КАК СуммаТекущая,
    СУММА(вт_ВсеДанные.СуммаПредыдущая) КАК СуммаПредыдущая,
    СУММА(вт_ВсеДанные.СуммаТекущая) - СУММА(вт_ВсеДанные.СуммаПредыдущая) КАК Разница
ИЗ
    вт_ВсеДанные КАК вт_ВсеДанные
СГРУППИРОВАТЬ ПО
    вт_ВсеДанные.СтраховойРиск
ИМЕЮЩИЕ
    СУММА(вт_ВсеДанные.СуммаТекущая) <> СУММА(вт_ВсеДанные.СуммаПредыдущая)

Преимущества схемы с ОБЪЕДИНИТЬ ВСЕ:

  1. Устойчивость к пустым таблицам. Если параметр &ПредыдущаяВерсия ссылается на пустой документ или не находит строк, вторая ветка объединения просто не добавит записей. Первая ветка отработает штатно, а поле СуммаПредыдущая для всех рисков будет равно нулю.
  2. Отсутствие NULL. В выборке участвуют явные числовые нули, что исключает ошибки сравнения и избавляет от необходимости применять функцию ЕСТЬNULL.
  3. Учет удаленных строк. Если риск присутствовал в прошлом документе, но отсутствует в текущем, он также попадет в отчет с отрицательной разницей.
  4. Оптимизация отбора. Фильтрация измененных строк выполняется в секции ИМЕЮЩИЕ по сгруппированным суммам.

Вариант 3. Оптимизация на уровне встроенного языка 1С

Выясним, как правильно передавать параметры в объект Запрос. Если мы не вызовем метод УстановитьПараметр() для параметра &ПредыдущаяВерсия, платформа завершит выполнение с системной ошибкой: «Не задано значение параметра». Передача значения Неопределено или пустой ссылки ошибку не вызывает, но заставляет СУБД выполнять заведомо пустой запрос к базе данных.

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


Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТекущаяВерсия", ТекущийДокументСсылка);

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

РезультатЗапроса = Запрос.Выполнить();

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

← На главную