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