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