В процессе разработки сложных отчетов на платформе 1С:Предприятие 8 программисты часто сталкиваются с ситуацией, когда добавление нового ЛЕВОЕ СОЕДИНЕНИЕ приводит к «исчезновению» данных из других таблиц или обнулению полей. Особенно часто это проявляется при работе с виртуальными таблицами остатков. Рассмотрим подробнее, почему это происходит и как правильно сконструировать запрос, чтобы информация выводилась корректно.
Разберем ситуацию, представленную в теме. Автор пытается собрать данные из трех разных источников: остатки по графику отгрузки, остатки в резервах на складах и данные по размещению заказов. В исходном варианте запроса использовались вложенные подзапросы (конструкции ВЫБРАТЬ ... ИЗ РегистрНакопления.Таблица.Остатки(...) КАК Подзапрос). Это одна из самых распространенных причин проблем с производительностью и корректностью данных.
Проанализируем, что происходит, когда мы соединяем несколько подзапросов напрямую в одном блоке ИЗ:
NULL. В языке запросов 1С любая операция (сложение, умножение, деление) с NULL дает в результате NULL. Если вы рассчитываете сумму или количество, используя поля из разных таблиц без обработки, результат может стать пустым.ГДЕ указано условие на поле из присоединенной таблицы (например, ЗапросРезерва.Склад = &Склад), то все строки, где резерва нет (т.е. поле равно NULL), будут отсечены. В итоге ЛЕВОЕ СОЕДИНЕНИЕ фактически превращается во ВНУТРЕННЕЕ.В ходе обсуждения выяснилось, что наиболее стабильный результат дает отказ от вложенных подзапросов в пользу прямого обращения к виртуальным таблицам или использования временных таблиц. Выясним причину, почему прямой запрос к виртуальной таблице РегистрНакопления...Остатки внутри соединения работает лучше: платформа 1С более эффективно транслирует такие конструкции в SQL-запрос, корректно обрабатывая параметры виртуальных таблиц.
Посмотрим на пример рабочего кода, к которому пришел автор в итоге:
ВЫБРАТЬ
ТоварыВГрафикеОтгрузкиОстатки.Номенклатура,
ТоварыВГрафикеОтгрузкиОстатки.ХарактеристикаНоменклатуры,
ТоварыВГрафикеОтгрузкиОстатки.Номенклатура.Родитель,
0 КАК Вес,
0 КАК Длина,
ВЫБОР
КОГДА ЗаказПокупателяТовары.Ссылка.СуммаВключаетНДС
ТОГДА ЗаказПокупателяТовары.Сумма / ЗаказПокупателяТовары.Количество * ТоварыВГрафикеОтгрузкиОстатки.КоличествоОстаток
ИНАЧЕ (ЗаказПокупателяТовары.Сумма + ЗаказПокупателяТовары.СуммаНДС) / ЗаказПокупателяТовары.Количество * ТоварыВГрафикеОтгрузкиОстатки.КоличествоОстаток
КОНЕЦ КАК Сумма,
ТоварыВГрафикеОтгрузкиОстатки.КоличествоОстаток КАК Количество,
ТоварыВРезервеНаСкладахОстатки.КоличествоОстаток КАК КоличествоРезерв,
РазмещениеЗаказовПокупателейОстатки.КоличествоОстаток КАК КоличествоЗаказано,
ТоварыВГрафикеОтгрузкиОстатки.Склад
ИЗ
РегистрНакопления.ТоварыВГрафикеОтгрузки.Остатки(&ДатаПечати, ЗаказПокупателя = &Заказ) КАК ТоварыВГрафикеОтгрузкиОстатки
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказПокупателя.Товары КАК ЗаказПокупателяТовары
ПО ТоварыВГрафикеОтгрузкиОстатки.ЗаказПокупателя = ЗаказПокупателяТовары.Ссылка
И ТоварыВГрафикеОтгрузкиОстатки.Номенклатура = ЗаказПокупателяТовары.Номенклатура
И ТоварыВГрафикеОтгрузкиОстатки.ХарактеристикаНоменклатуры = ЗаказПокупателяТовары.ХарактеристикаНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.РазмещениеЗаказовПокупателей.Остатки(&ДатаПечати, ЗаказПокупателя = &Заказ) КАК РазмещениеЗаказовПокупателейОстатки
ПО ТоварыВГрафикеОтгрузкиОстатки.Номенклатура = РазмещениеЗаказовПокупателейОстатки.Номенклатура
И ТоварыВГрафикеОтгрузкиОстатки.ХарактеристикаНоменклатуры = РазмещениеЗаказовПокупателейОстатки.ХарактеристикаНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыВРезервеНаСкладах.Остатки(&ДатаПечати, ДокументРезерва = &Заказ) КАК ТоварыВРезервеНаСкладахОстатки
ПО ТоварыВГрафикеОтгрузкиОстатки.Номенклатура = ТоварыВРезервеНаСкладахОстатки.Номенклатура
И ТоварыВГрафикеОтгрузкиОстатки.ХарактеристикаНоменклатуры = ТоварыВРезервеНаСкладахОстатки.ХарактеристикаНоменклатуры
И ТоварыВГрафикеОтгрузкиОстатки.Склад = ТоварыВРезервеНаСкладахОстатки.Склад
ГДЕ
ТоварыВГрафикеОтгрузкиОстатки.ДатаПланируемойОтгрузки МЕЖДУ &Дата1 И &Дата2
И ТоварыВГрафикеОтгрузкиОстатки.Склад В(&Склады)
И ТоварыВГрафикеОтгрузкиОстатки.КоличествоОстаток > 0
И ТоварыВГрафикеОтгрузкиОстатки.График.КомплекснаяОтгрузка = ИСТИНА
Хотя прямой запрос к виртуальным таблицам в соединениях допустим, стандартом разработки в среде 1С считается использование временных таблиц. Проанализируем ситуацию: когда мы соединяем таблицы остатков напрямую, СУБД выполняет расчет итогов для каждого регистра одновременно с выполнением условий соединения. Это может быть медленно.
Профессиональный подход заключается в следующем:
РегистрНакопления.ТоварыВГрафикеОтгрузки.Остатки во временную таблицу ВТ_График.ВТ_Резервы и ВТ_Размещение).Номенклатура, Характеристика, Склад.Пример использования функции ЕСТЬNULL для предотвращения потери данных:
При выводе полей из левых соединений всегда используйте проверку на пустое значение. Вместо прямого обращения к полю КоличествоОстаток, пишите ЕСТЬNULL(РазмещениеЗаказовПокупателейОстатки.КоличествоОстаток, 0). Это гарантирует, что если по товару нет размещения, в отчете будет стоять «0», а не пустое значение, которое может «сломать» дальнейшие расчеты в коде или в макете.
Важное замечание: Обратите внимание на условие по складу. В запросе автора условие ТоварыВГрафикеОтгрузкиОстатки.Склад В(&Склады) стоит в секции ГДЕ. Это корректно, так как ТоварыВГрафикеОтгрузкиОстатки является основной (левой) таблицей. Однако условия по документам-фильтрам (например, ЗаказПокупателя = &Заказ) крайне желательно передавать именно в параметры виртуальной таблицы, а не в условия связи ПО или ГДЕ. Это значительно ускоряет получение остатков, так как отсечение данных происходит на этапе обращения к итогам регистра, а не после получения всех записей из базы.
Таким образом, для получения стабильного результата всегда проверяйте типы соединений, обрабатывайте NULL через ЕСТЬNULL и старайтесь использовать временные таблицы для сложных расчетов.