Как правильно соединить таблицы в запросе 1С, чтобы избежать дублирования строк при связях "один-ко-многим"?

Программист 1С v8.3 (Управляемые формы) Управленческий учет Торговля и дистрибуция
← На главную

При работе с запросами в 1С разработчики часто сталкиваются с проблемой "перемножения строк" или нежелательного дублирования данных. Эта ситуация особенно характерна для сценариев, когда мы пытаемся соединить несколько сущностей, между которыми существуют связи типа "один-ко-многим". Давайте разберем подробнее, почему возникает такая проблема и как ее эффективно решить на примере запроса, описанного в теме форума.

Понимание проблемы: перемножение строк в запросе

Представим себе следующую структуру данных, как описано в исходной задаче:

  1. Документ Отправка.
  2. Документ КорректировкаОтправок: в его табличной части хранятся товары и ссылка на документ Отправка. При этом на одну Отправку может быть создано несколько КорректировокОтправок.
  3. Документ ТребованиеОбОплате: в его реквизите ДокументОснование хранится ссылка на документ Отправка.

Автор запроса хотел получить данные о требованиях к оплате, связанных с отправками, и при этом увидеть все корректировки, относящиеся к этим отправкам. Проблема возникла, когда к базовым строкам типа "Требование - Отправка - Сумма" стали присоединяться корректировки. Изначально, для одной Отправки 1 существуют две Корректировки (Корректировка 1 и Корректировка 2). Также, в примере автора, для Требования 1, связанного с Отправкой 1, существуют две суммы (200 и 100), что указывает либо на две разные строки в Требовании, либо на два разных Требования, которые по условиям выборки сгруппировались как "Требование 1".

Исходный запрос автора выдавал следующий результат (упрощенно):

  1. Требование 1 - Отправка 1 - Корректировка 1 - Сумма 200 - Правильно
  2. Требование 1 - Отправка 1 - Корректировка 2 - Сумма 200 - Не правильно
  3. Требование 1 - Отправка 1 - Корректировка 1 - Сумма 100 - Правильно
  4. Требование 1 - Отправка 1 - Корректировка 2 - Сумма 100 - Не правильно

Почему это происходит? Давайте проанализируем фрагмент запроса, отвечающий за соединение с корректировками:


ЛЕВОЕ СОЕДИНЕНИЕ вт_КорректировкиОтправок КАК вт_КорректировкиОтправок
ПО вт_КорректировкиОтправок.Отправка = ДокОтправка.Ссылка

Здесь происходит следующее: для каждой строки из левой части соединения (которая уже содержит информацию о "Требование - Отправка - Сумма"), система пытается найти все соответствующие записи из временной таблицы вт_КорректировкиОтправок, где поле Отправка совпадает со Ссылкой из ДокОтправка. Поскольку на одну ДокОтправка.Ссылка (например, Отправка 1) может приходиться несколько записей в вт_КорректировкиОтправок (Корректировка 1, Корректировка 2), каждая строка из левой части дублируется столько раз, сколько корректировок найдено. Это и есть "перемножение строк".

Важный момент, который также был затронут в обсуждении форума (сообщение 10), — это происхождение поля Сумма. Если Требование.Сумма является реквизитом шапки документа ТребованиеОбОплате, то это одна общая сумма для всего документа. Присоединяя к ней каждую корректировку, мы видим эту же общую сумму, что и создает ощущение "неправильности", так как сумма документа не "относится" к каждой конкретной корректировке.

Основные причины возникновения декартова произведения и дублирования строк

Прежде чем перейти к решениям, давайте кратко перечислим основные причины, по которым могут возникать нежелательные дублирования в запросах:

  1. Декартово произведение (перекрестное соединение): Возникает, когда в запросе указывается несколько источников данных без явного условия их соединения (например, таблицы перечислены через запятую в секции ИЗ). Каждая запись из одной таблицы комбинируется с каждой записью из другой.
  2. Соединение "один-ко-многим" без агрегации или уточнения: Это наиболее частая причина. Если одна запись из левой таблицы связана с несколькими записями из правой, то при простом соединении запись из левой таблицы будет дублироваться для каждой соответствующей записи из правой.
  3. Недостаточные условия соединения: Даже при использовании явных соединений (ВНУТРЕННЕЕ СОЕДИНЕНИЕ, ЛЕВОЕ СОЕДИНЕНИЕ) условие в секции ПО может быть недостаточно специфичным. Если оно не включает все необходимые поля для однозначной связи, это также может привести к дублированию.

Решение проблемы: Выбор стратегии формирования запроса

Для того чтобы "избавиться от не правильных строк", нам необходимо четко определить, какой именно результат мы хотим получить. В зависимости от этого, мы выберем одну из следующих стратегий:

  1. Получение уникальных базовых строк с агрегацией связанных данных: Если нам нужна одна строка для каждой комбинации "Требование - Отправка - Сумма", а информацию о корректировках нужно лишь подытожить (например, посчитать их количество или собрать в строку).
  2. Вывод всех связанных корректировок для каждой базовой строки: Если нам *действительно* нужно видеть каждую корректировку как отдельную строку, но при этом понять, как Сумма относится к ним. В этом случае, возможно, "дублирование" является корректным отображением, но Сумма должна быть более гранулярной (например, Сумма конкретной операции, а не документа).

Мы сосредоточимся на первом, наиболее распространенном сценарии, поскольку именно он часто воспринимается как "неправильный" из-за повторения общих данных.

Стратегия 1: Получение уникальных базовых строк с агрегацией связанных данных

Эта стратегия подходит, когда мы хотим получить одну уникальную запись по основным сущностям (например, "Требование - Отправка") и при этом получить сводную информацию о связанных сущностях (например, "Корректировках"). Мы будем использовать оператор СГРУППИРОВАТЬ ПО (GROUP BY) и агрегатные функции.

Разберем решение по шагам:

  1. Создание временной таблицы с корректировками.

    Эту часть запроса оставим как у автора, так как она корректно собирает информацию о корректировках и ссылках на отправки. Она готовит нам список всех корректировок с их родительскими отправками.

    
    ВЫБРАТЬ РАЗЛИЧНЫЕ
        ДокКорр.Ссылка КАК Ссылка,
        ДокКорр.НакладнаяПоставщика КАК НакладнаяПоставщика,
        КоррСостав.Отправка КАК Отправка
    ПОМЕСТИТЬ вт_КорректировкиОтправок
    ИЗ
        Документ.КорректировкаОтправок.Состав КАК КоррСостав
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.КорректировкаОтправок КАК ДокКорр
        ПО КоррСостав.Ссылка = ДокКорр.Ссылка
    ГДЕ
        ДокКорр.Проведен
        И ДокКорр.ВидКорректировки = ЗНАЧЕНИЕ(Перечисление.ВидыКорректировок.Отгрузка)
    ;
    

    Мы используем РАЗЛИЧНЫЕ, чтобы убедиться, что каждая комбинация "Корректировка - Отправка" уникальна, даже если в ТЧ одной корректировки несколько раз встречается одна и та же отправка (что маловероятно, но возможно).

  2. Основной запрос с группировкой.

    Теперь, когда у нас есть список корректировок, мы можем присоединить его к основному запросу, но применить группировку, чтобы избежать перемножения базовых строк.

    
    ВЫБРАТЬ
        Требование.Ссылка КАК Требование,
        ДокОтправка.Ссылка КАК Отправка,
        Требование.Сумма КАК Сумма,
        КОЛИЧЕСТВО(РАЗЛИЧНЫЕ вт_КорректировкиОтправок.Ссылка) КАК КоличествоКорректировок,
        МАКСИМУМ(вт_КорректировкиОтправок.Ссылка) КАК ПроизвольнаяКорректировка,
        МИНИМУМ(вт_КорректировкиОтправок.Ссылка) КАК ПерваяКорректировка
        // Если ваша версия платформы 1С поддерживает, можно использовать СТРОКА_СОЕДИНИТЬ:
        // СТРОКА_СОЕДИНИТЬ(РАЗЛИЧНЫЕ вт_КорректировкиОтправок.Ссылка, ", ") КАК ВсеКорректировкиСтрокой
    ИЗ
        Документ.ТребованиеОбОплате КАК Требование
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.Отправка КАК ДокОтправка
        ПО Требование.ДокументОснование = ДокОтправка.Ссылка
        ЛЕВОЕ СОЕДИНЕНИЕ вт_КорректировкиОтправок КАК вт_КорректировкиОтправок
        ПО вт_КорректировкиОтправок.Отправка = ДокОтправка.Ссылка
    ГДЕ
        Требование.ДокументОснование ССЫЛКА Документ.Отправка
        И ДокОтправка.Проведен
    СГРУППИРОВАТЬ ПО
        Требование.Ссылка,
        ДокОтправка.Ссылка,
        Требование.Сумма
    

    Рассмотрим изменения и их назначение:

    • Требование.Ссылка КАК Требование, ДокОтправка.Ссылка КАК Отправка, Требование.Сумма КАК Сумма: Эти поля образуют нашу базовую, уникальную строку. Мы включаем их в секцию ВЫБРАТЬ и обязательно дублируем в СГРУППИРОВАТЬ ПО.
    • КОЛИЧЕСТВО(РАЗЛИЧНЫЕ вт_КорректировкиОтправок.Ссылка) КАК КоличествоКорректировок: Эта агрегатная функция подсчитывает количество уникальных корректировок для каждой сгруппированной строки "Требование - Отправка - Сумма". Теперь мы видим, сколько корректировок относится к данной отправке, не дублируя при этом всю строку.
    • МАКСИМУМ(вт_КорректировкиОтправок.Ссылка) КАК ПроизвольнаяКорректировка / МИНИМУМ(вт_КорректировкиОтправок.Ссылка) КАК ПерваяКорректировка: Если нам нужно получить ссылку на *одну* из корректировок (например, самую "раннюю" или "позднюю" по ссылке, что не всегда имеет логический смысл, но может быть использовано как пример), мы можем использовать агрегатные функции МАКСИМУМ или МИНИМУМ.
    • СТРОКА_СОЕДИНИТЬ(РАЗЛИЧНЫЕ вт_КорректировкиОтправок.Ссылка, ", ") КАК ВсеКорректировкиСтрокой: Это очень удобная функция (доступна в относительно новых версиях платформы 1С), которая позволяет собрать все уникальные ссылки на корректировки в одну строку, разделенную указанным символом (в данном случае - ", "). Это дает полный список корректировок без дублирования строк.
    • СГРУППИРОВАТЬ ПО Требование.Ссылка, ДокОтправка.Ссылка, Требование.Сумма: Это ключевая часть решения. Мы указываем, что хотим получить уникальные комбинации именно по этим полям. Все остальные поля в ВЫБРАТЬ должны быть либо агрегатными функциями, либо также перечислены в СГРУППИРОВАТЬ ПО.

    Важный нюанс: Если Сумма берется из табличной части

    Если Требование.Сумма на самом деле является суммой строки табличной части ТребованиеОбОплате.СоставОпераций, а не реквизитом шапки документа, то структуру запроса нужно немного изменить, чтобы группировка происходила по уникальной строке табличной части.

    
    ВЫБРАТЬ РАЗЛИЧНЫЕ
        ДокКорр.Ссылка КАК Ссылка,
        КоррСостав.Отправка КАК Отправка
    ПОМЕСТИТЬ вт_КорректировкиОтправок
    ИЗ
        Документ.КорректировкаОтправок.Состав КАК КоррСостав
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.КорректировкаОтправок КАК ДокКорр
        ПО КоррСостав.Ссылка = ДокКорр.Ссылка
    ГДЕ
        ДокКорр.Проведен
        И ДокКорр.ВидКорректировки = ЗНАЧЕНИЕ(Перечисление.ВидыКорректировок.Отгрузка)
    ;
    ////////////////////////////////////////////////////////////
    ВЫБРАТЬ
        Требование.Ссылка КАК Требование,
        ТребованиеСостав.Ссылка КАК ТребованиеСтрока, // Добавляем ссылку на строку ТЧ для уникальности
        ДокОтправка.Ссылка КАК Отправка,
        ТребованиеСостав.Сумма КАК СуммаОперации, // Используем сумму из ТЧ
        КОЛИЧЕСТВО(РАЗЛИЧНЫЕ вт_КорректировкиОтправок.Ссылка) КАК КоличествоКорректировок,
        МАКСИМУМ(вт_КорректировкиОтправок.Ссылка) КАК ПроизвольнаяКорректировка
    ИЗ
        Документ.ТребованиеОбОплате КАК Требование
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ТребованиеОбОплате.СоставОпераций КАК ТребованиеСостав
        ПО ТребованиеСостав.Ссылка = Требование.Ссылка // Соединяемся с ТЧ для получения суммы
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.Отправка КАК ДокОтправка
        ПО Требование.ДокументОснование = ДокОтправка.Ссылка
        ЛЕВОЕ СОЕДИНЕНИЕ вт_КорректировкиОтправок КАК вт_КорректировкиОтправок
        ПО вт_КорректировкиОтправок.Отправка = ДокОтправка.Ссылка
    ГДЕ
        Требование.ДокументОснование ССЫЛКА Документ.Отправка
        И ДокОтправка.Проведен
    СГРУППИРОВАТЬ ПО
        Требование.Ссылка,
        ТребованиеСостав.Ссылка, // Группируем по уникальной строке ТЧ, чтобы не терять различия по сумме
        ДокОтправка.Ссылка,
        ТребованиеСостав.Сумма
    

    В этом случае мы включаем ТребованиеСостав.Ссылка в группировку, чтобы каждая строка табличной части Требования (с ее уникальной суммой) рассматривалась как отдельная базовая сущность перед агрегацией корректировок.

Стратегия 2: Вывод всех связанных корректировок для каждой базовой строки

Иногда требуется видеть каждую корректировку как отдельную строку. Если это ваш случай, и вы хотите, чтобы `Требование 1 - Отправка 1 - Сумма 200` повторялось для каждой корректировки, то ваш исходный запрос практически верен, но нужно понимать, что это не "неправильные" строки, а прямое следствие связи "один-ко-многим".

В этом сценарии важно задать себе вопрос: относится ли `Сумма` из `ТребованиеОбОплате` к каждой конкретной `КорректировкеОтправок`?

Если вы хотите получить каждую отдельную корректировку для каждого `Требования` и `Отправки`, но при этом избавиться от "ненужных" дублей, когда все остальные поля (кроме корректировки) полностью совпадают, вы можете добавить оператор РАЗЛИЧНЫЕ в самом начале секции ВЫБРАТЬ. Однако будьте осторожны: РАЗЛИЧНЫЕ удаляет только *полностью идентичные* строки. Если корректировки различаются, строки все равно будут присутствовать.


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Требование.Ссылка КАК Требование,
    ДокОтправка.Ссылка КАК Отправка,
    вт_КорректировкиОтправок.Ссылка КАК Корректировка,
    Требование.Сумма КАК Сумма
ИЗ
    Документ.ТребованиеОбОплате КАК Требование
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ТребованиеОбОплате.СоставОпераций КАК ТребованиеСостав // Если сумма из ТЧ
    ПО ТребованиеСостав.Ссылка = Требование.Ссылка
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Отправка КАК ДокОтправка
    ПО (НЕ Требование.ДокументОснование ЕСТЬ NULL)
    И (Требование.ДокументОснование ССЫЛКА Документ.Отправка)
    И (ДокОтправка.Ссылка = Требование.ДокументОснование)
    И (ДокОтправка.Проведен)
    ЛЕВОЕ СОЕДИНЕНИЕ вт_КорректировкиОтправок КАК вт_КорректировкиОтправок
    ПО вт_КорректировкиОтправок.Отправка = ДокОтправка.Ссылка

Этот запрос все равно будет генерировать дублирование, если на одну комбинацию "Требование - Отправка - Сумма" приходится несколько корректировок. Если вы считаете эти строки "неправильными", то вам нужна стратегия 1.

Рекомендации по оптимизации запросов и работе с временными таблицами

Для создания эффективных и корректных запросов в 1С всегда следует придерживаться следующих рекомендаций:

  1. Уточняйте условия соединения.

    Всегда используйте максимально точные условия в секции ПО, включая все поля, которые однозначно идентифицируют связанные записи. Чем точнее условие, тем меньше вероятность получить лишние строки.

  2. Используйте РАЗЛИЧНЫЕ осознанно.

    Оператор РАЗЛИЧНЫЕ (DISTINCT) убирает полностью совпадающие строки из результата. Однако его использование может скрыть логические ошибки в построении запроса, а также быть ресурсоемким для больших объемов данных. Применяйте его, когда вы точно знаете, что дубликаты не нужны и не являются следствием ошибки в логике.

  3. Разбивайте сложные запросы на временные таблицы.

    Использование временных таблиц (ПОМЕСТИТЬ) делает запрос более понятным, упрощает отладку и часто повышает производительность. Это позволяет контролировать количество строк на каждом этапе и избежать сложных вложенных запросов. Стандарты 1С активно рекомендуют временные таблицы вместо вложенных запросов.

  4. Тщательно анализируйте поля выборки и условий.

    Убедитесь, что каждое поле, которое вы выбираете или используете в условиях соединения, имеет смысл в контексте вашей задачи. Если поле Сумма является реквизитом документа, а не табличной части, то оно будет одинаковым для всех строк, относящихся к одному документу. Понимание этого поможет избежать ложных представлений о "неправильном" дублировании.

  5. Используйте агрегатные функции для сводной информации.

    Если вам нужна одна строка на каждую "базовую" сущность (например, "Требование - Отправка"), и при этом есть множество связанных "Корректировок", используйте агрегатные функции, такие как КОЛИЧЕСТВО, МАКСИМУМ, МИНИМУМ или СТРОКА_СОЕДИНИТЬ, чтобы получить сводную информацию о связанных объектах.

  6. Последовательность соединений.

    Иногда порядок соединений имеет значение для производительности и читаемости. Построение запроса в несколько этапов, сначала объединяя наиболее "уникальные" сущности, а затем добавляя остальные, может помочь.

  7. Используйте ВЫРАЗИТЬ для составных типов.

    Если вы работаете с полями составного типа, используйте функцию ВЫРАЗИТЬ для явного указания типа. Это помогает оптимизатору запросов работать более эффективно и предотвращает неявные ошибки.

Заключение

Проблема перемножения строк в запросах 1С является частым явлением при работе со сложными схемами данных. Как мы с вами выяснили, "правильное соединение" определяется не только синтаксически, но и логически — тем, какой именно результат вы хотите получить. Начиная с четкого определения желаемой структуры выходных данных, вы сможете выбрать наиболее подходящую стратегию (агрегацию, специфические условия соединения или использование вспомогательных временных таблиц), чтобы построить эффективный и корректный запрос.

В нашем случае, когда для одного "Требования" и "Отправки" появляется несколько "Корректировок", и при этом `Сумма` является общей для всего "Требования", наиболее логичным решением является группировка базовых данных и агрегация информации о корректировках. Это позволяет получить чистые, недублирующиеся строки с необходимой сводной информацией.

← На главную