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