Почему в запросе 1С пропадают данные при соединении нескольких виртуальных таблиц остатков?

Программист 1С v8.3 (Обычные формы) Управленческий учет Торговля и дистрибуция
← На главную

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

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

Разберем ситуацию, представленную в теме. Автор пытается собрать данные из трех разных источников: остатки по графику отгрузки, остатки в резервах на складах и данные по размещению заказов. В исходном варианте запроса использовались вложенные подзапросы (конструкции ВЫБРАТЬ ... ИЗ РегистрНакопления.Таблица.Остатки(...) КАК Подзапрос). Это одна из самых распространенных причин проблем с производительностью и корректностью данных.

Проанализируем, что происходит, когда мы соединяем несколько подзапросов напрямую в одном блоке ИЗ:

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

Рассмотрим правильную структуру запроса

В ходе обсуждения выяснилось, что наиболее стабильный результат дает отказ от вложенных подзапросов в пользу прямого обращения к виртуальным таблицам или использования временных таблиц. Выясним причину, почему прямой запрос к виртуальной таблице РегистрНакопления...Остатки внутри соединения работает лучше: платформа 1С более эффективно транслирует такие конструкции в SQL-запрос, корректно обрабатывая параметры виртуальных таблиц.

Посмотрим на пример рабочего кода, к которому пришел автор в итоге:


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

Разберем важные нюансы оптимизации

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

Профессиональный подход заключается в следующем:

  1. Сначала мы выбираем данные из РегистрНакопления.ТоварыВГрафикеОтгрузки.Остатки во временную таблицу ВТ_График.
  2. Затем выбираем данные из резервов и размещения в отдельные временные таблицы (например, ВТ_Резервы и ВТ_Размещение).
  3. Обязательно индексируем временные таблицы по полям связи: Номенклатура, Характеристика, Склад.
  4. В финальном запросе соединяем уже подготовленные временные таблицы.

Пример использования функции ЕСТЬNULL для предотвращения потери данных:

При выводе полей из левых соединений всегда используйте проверку на пустое значение. Вместо прямого обращения к полю КоличествоОстаток, пишите ЕСТЬNULL(РазмещениеЗаказовПокупателейОстатки.КоличествоОстаток, 0). Это гарантирует, что если по товару нет размещения, в отчете будет стоять «0», а не пустое значение, которое может «сломать» дальнейшие расчеты в коде или в макете.

Важное замечание: Обратите внимание на условие по складу. В запросе автора условие ТоварыВГрафикеОтгрузкиОстатки.Склад В(&Склады) стоит в секции ГДЕ. Это корректно, так как ТоварыВГрафикеОтгрузкиОстатки является основной (левой) таблицей. Однако условия по документам-фильтрам (например, ЗаказПокупателя = &Заказ) крайне желательно передавать именно в параметры виртуальной таблицы, а не в условия связи ПО или ГДЕ. Это значительно ускоряет получение остатков, так как отсечение данных происходит на этапе обращения к итогам регистра, а не после получения всех записей из базы.

Таким образом, для получения стабильного результата всегда проверяйте типы соединений, обрабатывайте NULL через ЕСТЬNULL и старайтесь использовать временные таблицы для сложных расчетов.

← На главную