При разработке отчетов на базе Системы компоновки данных (СКД) программисты часто сталкиваются с неприятной ситуацией: итоговые суммы в колонках начинают «раздуваться», умножаясь в несколько раз. Например, вместо ожидаемых 500 000 рублей в отчете выводится 2 000 000. Рассмотрим подробно, почему это происходит, проанализируем конкретный пример кода и разберем правильные методы решения этой проблемы.
Корень проблемы практически всегда кроется в логике построения запроса, а именно в использовании соединений (JOIN). Когда мы соединяем основную таблицу (где хранятся суммы) с дополнительной таблицей (аналитикой, статусами, свойствами), общее количество строк в результате может увеличиться. Это происходит, если одной записи из левой таблицы соответствует две или более записи из правой таблицы.
Проанализируем ситуацию на примере залоговых билетов. Допустим, у нас есть один залоговый билет на сумму 1000 рублей. Мы хотим вывести в отчет его статус. Если в таблице статусов для этого билета по ошибке или в силу логики данных окажется две записи (например, статус «Активен» и признак «Лояльный клиент»), то после соединения ЛЕВОЕ СОЕДИНЕНИЕ система создаст две строки. В каждой из этих строк будет стоять сумма 1000 рублей. При расчете итогов СКД просто сложит эти значения, и мы получим 2000 рублей вместо 1000.
Посмотрим на фрагмент запроса, который вызвал проблему у автора темы. В нем формируется временная таблица вт_Статусы с помощью оператора ОБЪЕДИНИТЬ ВСЕ:
ВЫБРАТЬ
ломб_СхемыКредитования.Контрагент.ИдентификационныйКодЛичности КАК ИИН,
"✔" КАК Индивидуальная,
"" КАК Лояльные,
"" КАК ЧерныйСписок
ПОМЕСТИТЬ вт_Статусы
ИЗ
Справочник.ломб_СхемыКредитования КАК ломб_СхемыКредитования
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ломб_СписокЗапрещенныхФизлиц.ИИН, "", "", "✔"
ИЗ
Справочник.ломб_СписокЗапрещенныхФизлиц КАК ломб_СписокЗапрещенныхФизлиц
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ломб_РазрешенныеСотрудникиЛьготноеКредитование.Сотрудник.ИдентификационныйКодЛичности, "", "✔", ""
ИЗ
РегистрСведений.ломб_РазрешенныеСотрудникиЛьготноеКредитование КАК ломб_РазрешенныеСотрудникиЛьготноеКредитование
Проблема здесь очевидна: один и тот же человек (с одним ИИН) может одновременно находиться в справочнике схем кредитования, в списке запрещенных лиц и быть сотрудником. Использование ОБЪЕДИНИТЬ ВСЕ создает для такого контрагента три разные строки в таблице вт_Статусы. Когда в финальном запросе происходит соединение по полю ИИН, каждая сумма по этому контрагенту утраивается.
Самый быстрый способ, к которому прибегнул автор темы, — добавление БЕЗ ПОВТОРЯЮЩИХСЯ (DISTINCT) в итоговом запросе. Рассмотрим, как это выглядит:
ВЫБРАТЬ РАЗЛИЧНЫЕ
вт_КонтрРазрешенные.ЗалоговыйБилетКонтрагент,
вт_КонтрРазрешенные.ЗалоговыйБилетСуммаКредита,
вт_Статусы.ИндивидуальныйКлиент,
...
Важный нюанс: хотя этот метод «схлопывает» одинаковые строки и суммы приходят в норму, он является скорее «костылем», чем профессиональным решением. При больших объемах данных DISTINCT значительно замедляет работу СУБД, так как системе приходится сравнивать каждое поле в каждой строке, чтобы найти дубликаты. Кроме того, если у одного клиента действительно было два разных залога на одинаковую сумму, DISTINCT может ошибочно объединить их, что приведет к занижению итогов.
Разберем более правильный подход. Нам нужно гарантировать, что в таблице вт_Статусы на один ИИН приходится ровно одна строка. Для этого необходимо сгруппировать данные перед тем, как соединять их с таблицей сумм. Перепишем блок формирования статусов:
ВЫБРАТЬ
Т.ИИН КАК ИИН,
МАКСИМУМ(Т.Индивидуальная) КАК Индивидуальная,
МАКСИМУМ(Т.Лояльные) КАК Лояльные,
МАКСИМУМ(Т.ЧерныйСписок) КАК ЧерныйСписок
ПОМЕСТИТЬ вт_Статусы_Сгруппированная
ИЗ (
ВЫБРАТЬ ИИН, Индивидуальная, Лояльные, ЧерныйСписок ИЗ вт_Статусы
) КАК Т
СГРУППИРОВАТЬ ПО
Т.ИИН;
Используя функцию МАКСИМУМ для строковых или булевых полей (галочек), мы собираем все признаки контрагента в одну строку. Теперь, сколько бы статусов ни было у клиента, в таблицу вт_Статусы_Сгруппированная попадет только одна запись для каждого ИИН, и суммы при соединении множиться не будут.
Иногда суммы множатся еще до всяких соединений. Проанализируем использование виртуальной таблицы СрезПоследних. Если регистр сведений, из которого мы берем данные, имеет несколько измерений (например, Контрагент и Организация), а мы делаем срез только по одному из них, система может вернуть несколько записей.
Всегда проверяйте, что условия в параметрах виртуальных таблиц и группировки в первом запросе (вт_ЗБ в примере автора) действительно обеспечивают уникальность ключа (например, ЗалоговыйБилет). Если один билет попадает в результат дважды из-за разных статусов в регистре, суммы «поплывут» уже на этом этапе.
Если структура данных такова, что дубли в запросе неизбежны (например, нам обязательно нужно видеть все записи аналитики в детальных записях, но итоги должны быть верными), мы можем воспользоваться ресурсами СКД. Рассмотрим два приема:
ЗалоговыйБилет роль «Без дублей» или явно определить его как ключевое поле. В некоторых случаях СКД сможет корректно рассчитать итоги, понимая иерархию данных.Сумма(СуммаКредита) можно использовать более сложную формулу на языке выражений СКД: Сумма(ВычислитьВыражениеСГруппировкойМассив("Максимум(СуммаКредита)", "ЗалоговыйБилет")). Это заставит систему сначала найти максимум суммы для каждого уникального билета, а затем сложить эти максимумы.Если вы столкнулись с умножением сумм в сложном отчете, выполните следующие шаги для диагностики:
ЛЕВОЕ СОЕДИНЕНИЕ с каждой таблицей. Если после отключения очередной таблицы сумма стала правильной — значит, именно в ней содержатся дубли по ключевому полю.Количество(*) и сгруппируйте данные по идентификатору (например, по ссылке на документ). Если в результате выполнения запроса вы видите число больше 1 напротив одного документа — вы нашли причину раздувания итогов.Контрагенты нет дублей. Часто бывает так, что заведены два разных элемента с одинаковым ИИН. В этом случае соединение по ИИН гарантированно даст удвоение строк.Подведем итог: для получения корректных данных в СКД необходимо обеспечивать уникальность записей в присоединяемых таблицах. Использование БЕЗ ПОВТОРЯЮЩИХСЯ допустимо для простых задач, но правильным архитектурным решением является предварительная группировка аналитических данных во временных таблицах по уникальному ключу связи.