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