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