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