При переносе или импорте данных в платформу 1С:Предприятие 8 из внешних источников (будь то файлы XML, таблицы Excel или базы данных устаревшей версии 1С 7.7) разработчики часто сталкиваются с крайне странным поведением системы. Попытка пакетно сопоставить контрагентов по ИНН (или УНН) с помощью классического SQL-запроса с условием В (&СпискеУНН) возвращает лишь часть результатов. При этом, если запустить резервный цикл поиска «в лоб» через метод НайтиПоРеквизиту(), платформа успешно находит всех недостающих контрагентов.
В этой статье мы подробно проанализируем, почему возникает такая разница в поведении запроса и объектного метода платформы, разберем скрытые механизмы работы СУБД и напишем оптимальный, высокопроизводительный алгоритм сопоставления данных, который работает без циклического обращения к базе данных.
На первый взгляд кажется, что запрос к СУБД и метод НайтиПоРеквизиту() выполняют абсолютно идентичную работу. Однако под капотом платформы процессы происходят совершенно по-разному. Выделим три ключевые причины, почему запрос терпит неудачу там, где объектный поиск справляется.
1. Строгое приведение типов СУБД против «умного» приведения платформы 1С
При импорте данных из XML (особенно выгруженных из 1С 7.7) типы в таблице значений часто сбиваются. Часть ИНН считывается как строки, а часть (состоящая только из цифр) автоматически преобразуется платформой в числа. Когда мы выгружаем колонку во встроенный массив и передаем его параметром в запрос, СУБД получает разнородную коллекцию. Поле ИНН в справочнике Контрагенты имеет строго строковый тип.
На уровне базы данных (MS SQL Server, PostgreSQL) сравнение строки и числа (например, '123456789' = 123456789) внутри оператора IN либо приводит к ошибке приведения типов, либо возвращает ложь (False). СУБД не делает неявного преобразования элементов массива при поиске по индексу.
Метод НайтиПоРеквизиту() работает иначе. Он выполняется на уровне сервера приложений 1С. Платформа обращается к метаданным конфигурации, определяет, что искомый реквизит имеет тип Строка, и самостоятельно, принудительно приводит переданный параметр к строковому представлению перед отправкой финального SQL-запроса. Поэтому поиск по числу через этот метод отрабатывает успешно.
2. Ловушка неразрывных пробелов (символ с ASCII-кодом 160)
При формировании числовых строк старые системы и внешние редакторы часто форматируют числа с разделителями групп разрядов. Вместо обычного пробела (код 32) используется неразрывный пробел (символ Символы.НПП, код 160).
Встроенная функция СокрЛП() удаляет только классические пробелы, символы табуляции и перевода строк. Неразрывные пробелы она игнорирует. В итоге в запрос уходит строка вида "123 456 789", которая никак не может совпасть с непрерывной строкой "123456789" в базе данных. В то же время, при поиске через НайтиПоРеквизиту() неявное приведение типа очищает форматирование по умолчанию (аналогично формату ЧГ=0), убирая разделители.
3. Специфика реквизитов фиксированной длины
В некоторых базах данных реквизит ИНН (или Код) имеет фиксированную длину (например, 12 символов). Если реальная длина ИНН составляет 9 символов, СУБД хранит его с дополненными справа пробелами: "123456789 ". При выполнении прямого SQL-запроса сравнение точной строки "123456789" со строкой фиксированной длины "123456789 " вернет ложь. Платформенный метод НайтиПоРеквизиту() автоматически учитывает особенности метаданных и дополняет искомую строку хвостовыми пробелами до нужной длины.
---
Для того чтобы запрос отработал со стопроцентной эффективностью, мы должны привести все значения во входящей таблице к единому, строго очищенному строковому формату. Выполним обход таблицы значений и удалим все пробельные символы (включая неразрывные) и приведем числа к строкам без разделения разрядов:
Для Каждого СтрокаДанных Из табЗнач Цикл
// Преобразуем значение в строку, жестко отключив группировку разрядов
ОчищеннаяСтрока = Формат(СтрокаДанных.УНН, "ЧГ=0");
// Удаляем обычные пробелы (код 32)
ОчищеннаяСтрока = СтрЗаменить(ОчищеннаяСтрока, " ", "");
// Удаляем неразрывные пробелы (код 160)
ОчищеннаяСтрока = СтрЗаменить(ОчищеннаяСтрока, Символы.НПП, "");
// Применяем стандартное сокращение пробелов и записываем обратно в ТЗ
СтрокаДанных.УНН = СокрЛП(ОчищеннаяСтрока);
КонецЦикла;
Этот простой шаг гарантирует, что в нашей таблице гарантированно лежат чистые строки без скрытого мусора и невидимых разделителей.
---
Использование метода НайтиПоРеквизиту() в цикле — это грубейшая архитектурная ошибка (антипаттерн «Запрос в цикле»). При наличии тысяч строк в импортируемом файле это приведет к тысячам отдельных транзакций к базе данных, перегрузит сервер и растянет процесс импорта на долгие минуты или даже часы.
Проанализируем правильный подход: мы передадим предварительно подготовленную таблицу значений в один единственный запрос, поместим её во временную таблицу базы данных, проиндексируем и выполним быстрое левое соединение со справочником контрагентов на стороне СУБД.
Перед передачей таблицы в запрос важно убедиться, что колонка таблицы типизирована. Если таблица получена динамически, типизируем её колонку:
// Создаем описание типов для колонки УНН
МассивТипов = Новый Массив;
МассивТипов.Добавить(Тип("Строка"));
ОписаниеТиповСтрока = Новый ОписаниеТипов(МассивТипов, , Новый КвалификаторыСтроки(20));
// Если колонка уже есть, но не типизирована, лучше пересоздать ТЗ с типизацией
табТипизированная = Новый ТаблицаЗначений;
табТипизированная.Колонки.Добавить("УНН", ОписаниеТиповСтрока);
табТипизированная.Колонки.Добавить("Контра", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
// Добавим служебную колонку для точного сопоставления строк обратно
табТипизированная.Колонки.Добавить("НомерСтрокиФайла", Новый ОписаниеТипов("Число"));
НомерСтроки = 0;
Для Каждого СтрокаИсходная Из табЗнач Цикл
НоваяСтрока = табТипизированная.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсходная);
НомерСтроки = НомерСтроки + 1;
НоваяСтрока.НомерСтрокиФайла = НомерСтроки;
КонецЦикла;
Теперь построим и выполним пакетный запрос к СУБД:
// Инициализируем запрос и менеджер временных таблиц
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// Передаем типизированную таблицу значений в параметры запроса
Запрос.УстановитьПараметр("ВходнаяТаблица", табТипизированная);
Запрос.Текст =
"ВЫБРАТЬ
| ВхДанные.УНН КАК УНН,
| ВхДанные.НомерСтрокиФайла КАК НомерСтрокиФайла
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ВходнаяТаблица КАК ВхДанные
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.НомерСтрокиФайла КАК НомерСтрокиФайла,
| ВТ.УНН КАК УНН,
| Контрагенты.Ссылка КАК КонтрагентСсылка
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
| ПО ВТ.УНН = Контрагенты.ИНН";
// Выполняем пакетный запрос
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// Заполняем ссылки найденных контрагентов в исходную таблицу
Для Каждого СтрокаРезультата Из РезультатЗапроса Цикл
Если ЗначениеЗаполнено(СтрокаРезультата.КонтрагентСсылка) Тогда
// Находим нужную строку по индексу строки файла для абсолютной точности
СтрокаТЗ = табТипизированная[СтрокаРезультата.НомерСтрокиФайла - 1];
СтрокаТЗ.Контра = СтрокаРезультата.КонтрагентСсылка;
КонецЕсли;
КонецЦикла;
ИНН. Обработка файла на 10 000 записей займет доли секунды вместо минут ожидания при поиске в цикле.Используя данный метод, вы навсегда избавитесь от мистического поведения поисковых алгоритмов и обеспечите максимально возможную производительность вашей интеграционной обработки в 1С.