Почему запрос с ЛЕВЫМ СОЕДИНЕНИЕМ возвращает больше строк, чем в исходной таблице?

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

При работе с запросами в 1С часто возникает задача сопоставления данных из разных источников. Классический пример: у нас есть таблица данных (например, загруженная из Excel), где наименования объектов представлены строкой, и нам нужно найти соответствующие им ссылки на элементы справочника в базе данных. Стандартный инструмент для этого — соединение таблиц. Однако иногда результат выполнения запроса может быть неожиданным. Рассмотрим ситуацию, когда на входе в запрос подается таблица с 49 строками, а на выходе получается 50 или более. Разберем, почему так происходит и как с этим бороться.

Анализ исходной проблемы

Автор столкнулся с классической ситуацией. Есть таблица значений ТаблицаОсновныхСредств, загруженная из внешнего источника. В ней 49 записей. Цель — для каждой строки найти соответствующий элемент в справочнике РасшАР_ОсновныеСредства, сопоставляя по полю Наименование. Для этого используется следующий запрос:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|    ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС КАК ИнвентарныйНомерОбъектаОС,
|    ТаблицаОсновныхСредств.ОсновноеСредство КАК ОсновноеСредство,
|    ТаблицаОсновныхСредств.ЗаводскойНомер КАК ЗаводскойНомер,
|    ТаблицаОсновныхСредств.СтоимостьОбъекта КАК СтоимостьОбъекта,
|    ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов КАК ФактическоеКоличествоОбъектов
|ПОМЕСТИТЬ ВТ_ТаблицаОсновныхСредств
|ИЗ
|    &ТаблицаОсновныхСредств КАК ТаблицаОсновныхСредств
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|    РасшАР_ОсновныеСредства.Ссылка КАК ОсновноеСредство,
|    ВТ_ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС КАК ИнвентарныйНомерОбъектаОС,
|    ВТ_ТаблицаОсновныхСредств.ЗаводскойНомер КАК ЗаводскойНомер,
|    ВТ_ТаблицаОсновныхСредств.СтоимостьОбъекта КАК СтоимостьОбъекта,
|    ВТ_ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов КАК ФактическоеКоличествоОбъектов,
|    РасшАР_ОсновныеСредства.Ссылка КАК Ссылка
|ИЗ
|    ВТ_ТаблицаОсновныхСредств КАК ВТ_ТаблицаОсновныхСредств
|        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.РасшАР_ОсновныеСредства КАК РасшАР_ОсновныеСредства
|        ПО ВТ_ТаблицаОсновныхСредств.ОсновноеСредство = РасшАР_ОсновныеСредства.Наименование";

Запрос.УстановитьПараметр("ТаблицаОсновныхСредств", ТаблицаОсновныхСредств);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();

В результате выполнения этого кода в итоговой таблице оказывается 50 строк. Давайте выясним причину.

Причина увеличения количества строк: связь "Один-ко-Многим"

Проблема кроется в механизме работы оператора ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN). Этот оператор берет каждую строку из левой таблицы (в нашем случае это временная таблица ВТ_ТаблицаОсновныхСредств) и ищет для нее все подходящие строки в правой таблице (Справочник.РасшАР_ОсновныеСредства) по заданному условию.

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

Простой пример:

  1. В вашей исходной таблице есть строка с наименованием "Термопринтер".
  2. В справочнике в базе данных по какой-то причине создано два элемента с одинаковым наименованием "Термопринтер".

В этом случае запрос для одной вашей строки найдет два соответствия и вернет две строки в итоговом результате. Именно это и произошло: для одной из 49 строк нашлось два элемента в справочнике, что и привело к появлению 50-й строки.

Решение 1: Группировка для получения уникальной ссылки

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

Логика следующая:

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

Посмотрим на пример работающего запроса:


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

Важный нюанс использования МИНИМУМ(Ссылка): Ссылки в 1С (GUID) сравниваются как 128-битные числа. Результат применения МИНИМУМ() или МАКСИМУМ() будет всегда одинаковым для одного и того же набора данных (детерминированным), но с точки зрения бизнес-логики выбор будет "случайным". Вы можете случайно привязать данные к устаревшему или помеченному на удаление элементу справочника. Если важно выбирать, например, не помеченный на удаление элемент, логику запроса для создания ВТ_УникальныеСсылки нужно усложнять.

Оптимизация производительности: Индексация

Для небольших таблиц (как 49 строк) производительность не является проблемой. Но если вы работаете с тысячами строк, скорость выполнения запроса становится критичной. Чтобы ускорить соединение, необходимо индексировать временные таблицы по тем полям, которые участвуют в условии связи.

В нашем случае, в первом пакете запроса следует добавить конструкцию ИНДЕКСИРОВАТЬ ПО.


ВЫБРАТЬ
    ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС,
    ТаблицаОсновныхСредств.ОсновноеСредство,
    ТаблицаОсновныхСредств.ЗаводскойНомер,
    ТаблицаОсновныхСредств.СтоимостьОбъекта,
    ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов
ПОМЕСТИТЬ ВТ_ТаблицаОсновныхСредств
ИЗ
    &ТаблицаОсновныхСредств КАК ТаблицаОсновныхСредств
ИНДЕКСИРОВАТЬ ПО
    ОсновноеСредство

Это создаст индекс по полю ОсновноеСредство, и СУБД сможет выполнить соединение со второй временной таблицей или справочником гораздо быстрее, используя поиск по индексу вместо полного перебора строк (Full Table Scan).

Дополнительные рекомендации: подготовка данных перед запросом

Соединение по строковым полям — одна из самых "хрупких" операций. Данные, пришедшие из Excel, часто содержат "мусор", который мешает корректному сопоставлению.

Решение: Перед тем как передавать таблицу значений в параметр запроса, необходимо "очистить" (санитизировать) данные. Как минимум, следует обработать ключевое поле функцией СокрЛП(), которая удаляет начальные и конечные пробелы.


Для Каждого Строка ИЗ ТаблицаОсновныхСредств Цикл
    Строка.ОсновноеСредство = СокрЛП(Строка.ОсновноеСредство);
КонецЦикла;

Запрос.УстановитьПараметр("ТаблицаОсновныхСредств", ТаблицаОсновныхСредств);

Эта простая операция может избавить от множества проблем с ненайденными соответствиями.

Альтернативный подход: программная обработка

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

  1. Выгружаем уникальные наименования из исходной таблицы в массив.
  2. Выполняем простой запрос к справочнику, чтобы получить все возможные соответствия: ВЫБРАТЬ Ссылка, Наименование, ПометкаУдаления ИЗ Справочник.Номенклатура ГДЕ Наименование В (&МассивНаименований).
  3. Выгружаем результат в таблицу значений или соответствие.
  4. В цикле обходим исходные данные и программно сопоставляем их с полученными из базы. Здесь вы можете реализовать любую логику: если найдено несколько ссылок, выбрать ту, что не помечена на удаление; если неоднозначность осталась — сообщить пользователю и попросить сделать выбор.

Этот подход дает больше гибкости, но обычно медленнее, чем один хорошо написанный пакетный запрос, особенно на больших объемах данных.

← На главную