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