При работе с запросами в 1С часто возникает задача сопоставления данных из разных источников. Классический пример: у нас есть таблица данных (например, загруженная из Excel), где наименования объектов представлены строкой, и нам нужно найти соответствующие им ссылки на элементы справочника в базе данных. Стандартный инструмент для этого — соединение таблиц. Однако иногда результат выполнения запроса может быть неожиданным. Рассмотрим ситуацию, когда на входе в запрос подается таблица с 49 строками, а на выходе получается 50 или более. Разберем, почему так происходит и как с этим бороться.
Автор столкнулся с классической ситуацией. Есть таблица значений ТаблицаОсновныхСредств, загруженная из внешнего источника. В ней 49 записей. Цель — для каждой строки найти соответствующий элемент в справочнике РасшАР_ОсновныеСредства, сопоставляя по полю Наименование. Для этого используется следующий запрос:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС КАК ИнвентарныйНомерОбъектаОС,
| ТаблицаОсновныхСредств.ОсновноеСредство КАК ОсновноеСредство,
| ТаблицаОсновныхСредств.ЗаводскойНомер КАК ЗаводскойНомер,
| ТаблицаОсновныхСредств.СтоимостьОбъекта КАК СтоимостьОбъекта,
| ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов КАК ФактическоеКоличествоОбъектов
|ПОМЕСТИТЬ ВТ_ТаблицаОсновныхСредств
|ИЗ
| &ТаблицаОсновныхСредств КАК ТаблицаОсновныхСредств
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| РасшАР_ОсновныеСредства.Ссылка КАК ОсновноеСредство,
| ВТ_ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС КАК ИнвентарныйНомерОбъектаОС,
| ВТ_ТаблицаОсновныхСредств.ЗаводскойНомер КАК ЗаводскойНомер,
| ВТ_ТаблицаОсновныхСредств.СтоимостьОбъекта КАК СтоимостьОбъекта,
| ВТ_ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов КАК ФактическоеКоличествоОбъектов,
| РасшАР_ОсновныеСредства.Ссылка КАК Ссылка
|ИЗ
| ВТ_ТаблицаОсновныхСредств КАК ВТ_ТаблицаОсновныхСредств
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.РасшАР_ОсновныеСредства КАК РасшАР_ОсновныеСредства
| ПО ВТ_ТаблицаОсновныхСредств.ОсновноеСредство = РасшАР_ОсновныеСредства.Наименование";
Запрос.УстановитьПараметр("ТаблицаОсновныхСредств", ТаблицаОсновныхСредств);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
В результате выполнения этого кода в итоговой таблице оказывается 50 строк. Давайте выясним причину.
Проблема кроется в механизме работы оператора ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN). Этот оператор берет каждую строку из левой таблицы (в нашем случае это временная таблица ВТ_ТаблицаОсновныхСредств) и ищет для нее все подходящие строки в правой таблице (Справочник.РасшАР_ОсновныеСредства) по заданному условию.
Если для одной строки из левой таблицы находится несколько совпадений в правой, то исходная строка будет продублирована столько раз, сколько совпадений найдено. Это называется увеличением мощности выборки при соединении.
Простой пример:
В этом случае запрос для одной вашей строки найдет два соответствия и вернет две строки в итоговом результате. Именно это и произошло: для одной из 49 строк нашлось два элемента в справочнике, что и привело к появлению 50-й строки.
Самый распространенный и правильный способ решить эту задачу — заранее подготовить данные из справочника так, чтобы для каждого наименования была только одна ссылка. Это достигается с помощью группировки и агрегатных функций, таких как МИНИМУМ() или МАКСИМУМ().
Логика следующая:
Посмотрим на пример работающего запроса:
ВЫБРАТЬ
ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС КАК ИнвентарныйНомерОбъектаОС,
ТаблицаОсновныхСредств.ОсновноеСредство КАК ОсновноеСредство,
ТаблицаОсновныхСредств.ЗаводскойНомер КАК ЗаводскойНомер,
ТаблицаОсновныхСредств.СтоимостьОбъекта КАК СтоимостьОбъекта,
ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов КАК ФактическоеКоличествоОбъектов
ПОМЕСТИТЬ ВТ_ТаблицаОсновныхСредств
ИЗ
&ТаблицаОсновныхСредств КАК ТаблицаОсновныхСредств
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
МИНИМУМ(РасшАР_ОсновныеСредства.Ссылка) КАК ОсновноеСредствоСсылка,
РасшАР_ОсновныеСредства.Наименование КАК Наименование
ПОМЕСТИТЬ ВТ_УникальныеСсылки
ИЗ
Справочник.РасшАР_ОсновныеСредства КАК РасшАР_ОсновныеСредства
СГРУППИРОВАТЬ ПО
РасшАР_ОсновныеСредства.Наименование
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_УникальныеСсылки.ОсновноеСредствоСсылка КАК ОсновноеСредство,
ВТ_ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС,
ВТ_ТаблицаОсновныхСредств.ЗаводскойНомер,
ВТ_ТаблицаОсновныхСредств.СтоимостьОбъекта,
ВТ_ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов
ИЗ
ВТ_ТаблицаОсновныхСредств КАК ВТ_ТаблицаОсновныхСредств
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_УникальныеСсылки КАК ВТ_УникальныеСсылки
ПО (ВТ_ТаблицаОсновныхСредств.ОсновноеСредство = ВТ_УникальныеСсылки.Наименование)
Важный нюанс использования МИНИМУМ(Ссылка): Ссылки в 1С (GUID) сравниваются как 128-битные числа. Результат применения МИНИМУМ() или МАКСИМУМ() будет всегда одинаковым для одного и того же набора данных (детерминированным), но с точки зрения бизнес-логики выбор будет "случайным". Вы можете случайно привязать данные к устаревшему или помеченному на удаление элементу справочника. Если важно выбирать, например, не помеченный на удаление элемент, логику запроса для создания ВТ_УникальныеСсылки нужно усложнять.
Для небольших таблиц (как 49 строк) производительность не является проблемой. Но если вы работаете с тысячами строк, скорость выполнения запроса становится критичной. Чтобы ускорить соединение, необходимо индексировать временные таблицы по тем полям, которые участвуют в условии связи.
В нашем случае, в первом пакете запроса следует добавить конструкцию ИНДЕКСИРОВАТЬ ПО.
ВЫБРАТЬ
ТаблицаОсновныхСредств.ИнвентарныйНомерОбъектаОС,
ТаблицаОсновныхСредств.ОсновноеСредство,
ТаблицаОсновныхСредств.ЗаводскойНомер,
ТаблицаОсновныхСредств.СтоимостьОбъекта,
ТаблицаОсновныхСредств.ФактическоеКоличествоОбъектов
ПОМЕСТИТЬ ВТ_ТаблицаОсновныхСредств
ИЗ
&ТаблицаОсновныхСредств КАК ТаблицаОсновныхСредств
ИНДЕКСИРОВАТЬ ПО
ОсновноеСредство
Это создаст индекс по полю ОсновноеСредство, и СУБД сможет выполнить соединение со второй временной таблицей или справочником гораздо быстрее, используя поиск по индексу вместо полного перебора строк (Full Table Scan).
Соединение по строковым полям — одна из самых "хрупких" операций. Данные, пришедшие из Excel, часто содержат "мусор", который мешает корректному сопоставлению.
Решение: Перед тем как передавать таблицу значений в параметр запроса, необходимо "очистить" (санитизировать) данные. Как минимум, следует обработать ключевое поле функцией СокрЛП(), которая удаляет начальные и конечные пробелы.
Для Каждого Строка ИЗ ТаблицаОсновныхСредств Цикл
Строка.ОсновноеСредство = СокрЛП(Строка.ОсновноеСредство);
КонецЦикла;
Запрос.УстановитьПараметр("ТаблицаОсновныхСредств", ТаблицаОсновныхСредств);
Эта простая операция может избавить от множества проблем с ненайденными соответствиями.
Для небольших объемов данных или в случаях, когда логика выбора "правильного" дубля сложна, можно отказаться от громоздкого запроса в пользу программной обработки.
ВЫБРАТЬ Ссылка, Наименование, ПометкаУдаления ИЗ Справочник.Номенклатура ГДЕ Наименование В (&МассивНаименований).Этот подход дает больше гибкости, но обычно медленнее, чем один хорошо написанный пакетный запрос, особенно на больших объемах данных.