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