При разработке отчетов в системе компоновки данных (СКД) на платформе 1С:Предприятие 8 программисты часто сталкиваются с ситуацией, когда отчет выводит данные некорректно: пропадают строки, не отображаются позиции без характеристик или ломается логика связей при установке пользовательских отборов. Особенно часто это происходит при соединении оборотных регистров (например, выпуск продукции) с регистрами сведений (например, цены номенклатуры).
В этой статье мы подробно разберем две критические технические ошибки, которые приводят к подобному поведению отчета, выясним причины их возникновения на уровне СУБД и рассмотрим по шагам правильные варианты построения запросов в СКД.
Рассмотрим классический сценарий. Нам необходимо построить отчет, который показывает объем выпущенной продукции и оценивает его по определенному виду цен (например, по дилерской цене). Для этого мы соединяем виртуальную таблицу оборотов регистра накопления ВыпускПродукции.Обороты и виртуальную таблицу среза последних регистра сведений ЦеныНоменклатуры.СрезПоследних.
В процессе работы мы можем столкнуться со следующими проблемами:
ЛЕВОЕ СОЕДИНЕНИЕ) начинает вести себя как внутреннее (ВНУТРЕННЕЕ СОЕДИНЕНИЕ).Давайте подробно проанализируем каждую из этих проблем и найдем надежное решение.
---
Рассмотрим фрагмент некорректного условия соединения, которое часто пишут начинающие разработчики:
ПО ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Номенклатура.Ссылка = ЦеныНоменклатурыСрезПоследних.Номенклатура.Ссылка
И ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Характеристика.Ссылка = ЦеныНоменклатурыСрезПоследних.Характеристика.Ссылка
На первый взгляд, это условие кажется логичным. Однако использование конструкции .Ссылка для полей, которые уже сами по себе являются ссылочными типами данных (таких как Номенклатура или Характеристика), таит в себе серьезную опасность.
Выясним причину, почему это ломает логику левого соединения для пустых характеристик:
1. Неявное левое соединение (Implicit Join) на уровне СУБД. Когда мы обращаемся к реквизиту через точку (в данном случае пишем Характеристика.Ссылка), платформа 1С вынуждена сгенерировать SQL-запрос, который делает дополнительное левое соединение с соответствующей таблицей справочника (в данном случае с таблицей характеристик). Это снижает производительность запроса и усложняет его структуру.
2. Обработка пустой ссылки. Если у товара не ведется учет по характеристикам, то в поле Характеристика хранится значение ЗначениеЗаполнено() = Ложь, то есть пустая ссылка данного типа (состоящая из одних нулей в уникальном идентификаторе). При попытке обратиться через точку к свойству .Ссылка пустой ссылки, СУБД пытается найти запись с нулевым идентификатором в таблице справочника характеристик. Естественно, такой записи там нет. В результате этого неявного соединения СУБД возвращает значение NULL.
3. Сравнение с NULL в языке SQL. В результирующем SQL-запросе наше условие соединения превращается в сравнение: NULL = NULL (или сравнение поля с NULL). По правилам трехзначной логики SQL, любое сравнение значения с NULL, а также сравнение NULL = NULL возвращает результат UNKNOWN (ложь). Из-за этого условие связи для товаров с пустой характеристикой никогда не выполняется, и левое соединение по этим строкам разрывается.
Решение проблемы №1: Никогда не используйте обращение через точку к полю Ссылка. Поля Номенклатура и Характеристика уже содержат ссылки. Сравнивать нужно непосредственно сами поля:
ПО ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
И ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика
В этом случае СУБД сравнивает физические значения идентификаторов в таблицах (включая пустые ссылки типа "00000000-..."), не обращаясь к таблицам справочников. Соединение отработает корректно, и товары без характеристик не пропадут.
---
Вторая проблема заключается в поведении отборов по правой (присоединяемой) таблице. Допустим, мы хотим видеть цены только определенного вида — "Дилерская".
Если мы просто добавим левое соединение с таблицей цен, а в настройках СКД (или в секции ГДЕ запроса) укажем отбор:
ЦеныНоменклатурыСрезПоследних.ВидЦены = &ВидЦены
то наш отчет моментально лишится всех строк, для которых цена "Дилерская" не установлена. Почему это происходит?
Механизм работы ЛЕВОГО СОЕДИНЕНИЯ с фильтрацией:
ВидЦены и Цена) возвращаются значения NULL.ГДЕ или наложенное СКД на весь результат запроса.NULL = "Дилерская".Почему без отбора цены выводятся пустыми, хотя они есть в системе?
Если мы уберем отбор по виду цен вообще, то виртуальная таблица СрезПоследних без параметров вернет последнюю по времени запись для каждого товара, независимо от вида цены. Если последней установленной ценой для товара была "Закупочная", то именно она вернется из среза. Если в самом отчете выводится колонка только для "Дилерской" цены, то поле останется пустым, так как дилерская цена не была последней в регистре.
Решение проблемы №2: Чтобы сохранить логику левого соединения и гарантировать вывод всех товаров (даже без цен), любые отборы по правой таблице необходимо накладывать до момента соединения — непосредственно в параметрах виртуальной таблицы правой части.
---
Рассмотрим по шагам, как правильно составить запрос и настроить систему компоновки данных для корректного вывода цен и товаров.
Для того чтобы ограничить срез цен только нужным видом цен и не потерять товары без цен, мы должны передать параметр ВидЦены в качестве условия самой виртуальной таблицы СрезПоследних.
Также рекомендуется использовать функцию ЕСТЬNULL, чтобы в отчете вместо пустых значений (NULL) для товаров без цен выводился аккуратный ноль.
Посмотрим на правильный текст запроса:
ВЫБРАТЬ
ВыпускПродукцииОбороты.Заказ КАК Заказ,
ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Номенклатура КАК Номенклатура,
ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Характеристика КАК Характеристика,
ВыпускПродукцииОбороты.КоличествоОборот КАК Количество,
ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена,
ВыпускПродукцииОбороты.КоличествоОборот * ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Сумма
ИЗ
РегистрНакопления.ВыпускПродукции.Обороты(, , Период, ) КАК ВыпускПродукцииОбороты
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
ПО ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
И ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика
Если мы хотим предоставить пользователю возможность гибко выбирать вид цен прямо в настройках отчета (на вкладке "Отбор"), но при этом сохранить корректность запроса, нам следует использовать механизм расширения языка запросов СКД — фигурные скобки {}.
Перепишем левое соединение с использованием фигурных скобок:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, {ВидЦены.* КАК ВидЦены}) КАК ЦеныНоменклатурыСрезПоследних
ПО ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
И ВыпускПродукцииОбороты.АналитикаУчетаНоменклатуры.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика
Разберем подробнее, как это работает:
{ВидЦены.* КАК ВидЦены} сообщает компоновщику макета СКД, что если пользователь установит отбор по полю ВидЦены в настройках отчета, то СКД должна автоматически внедрить это условие во второй параметр виртуальной таблицы СрезПоследних.ГДЕ основного запроса этот отбор накладываться не будет!ЛЕВОГО СОЕДИНЕНИЯ полностью сохраняется: в отчет попадут абсолютно все товары из выпуска продукции, а цены подтянутся только для выбранного пользователем вида цен. Для товаров, у которых нет цен выбранного вида, колонки цены и суммы останутся пустыми или заполнятся нулями.При проектировании сложных отчетов в 1С:ERP и других конфигурациях на платформе 1С:Предприятие 8 всегда придерживайтесь следующих правил:
Номенклатура = Номенклатура), чтобы избежать проблем с обработкой пустых ссылок на уровне СУБД.СрезПоследних, Обороты, ОстаткиИОбороты).{} позволяют перенаправить пользовательские отборы из общей секции фильтрации непосредственно внутрь виртуальных таблиц, гарантируя высокую производительность СУБД и правильное поведение соединений.ЕСТЬNULL(Поле, 0) для предотвращения ошибок при расчете математических выражений в запросе.