В практике разработки на платформе 1С часто возникает задача поиска объектов, которые содержат определенный набор признаков. Ситуация осложняется, когда количество этих признаков заранее не ограничено (сегодня их 5, завтра 30), а общее число полей в справочнике достигает сотни. При этом база данных может содержать сотни тысяч или миллионы записей. В этой статье мы разберем, почему архитектура со множеством реквизитов является тупиковой, и проанализируем наиболее эффективные способы решения задачи ранжированного поиска.
Рассмотрим ситуацию, когда в справочнике создано 100 реквизитов для хранения значений. Проанализируем основные недостатки такого подхода. Во-первых, это раздувание таблиц (Database Bloat). Даже если большинство реквизитов пусты, СУБД резервирует место под структуру записи, что замедляет сканирование таблиц. Во-вторых, построить эффективный индекс по 100 полям невозможно. В-третьих, написание запроса, который перебирает все 100 полей и сравнивает их с массивом входящих данных, превращается в технический кошмар.
Если мы попытаемся использовать оператор ГДЕ для всех полей, запрос будет выглядеть громоздко и работать крайне медленно. Поэтому первым делом мы должны рассмотреть изменение структуры хранения данных.
Вместо того чтобы хранить данные в «горизонтальной» структуре (много колонок), перейдем к «вертикальной». Мы создадим непериодический независимый Регистр сведений (например, ЗначенияОбъектов) со следующей структурой:
Объект (Ссылка на наш основной справочник).Значение (Тип данных, соответствующий искомым переменным).Важно установить признак Индексировать для измерения Значение. Это позволит SQL-серверу мгновенно находить все объекты, связанные с конкретным признаком. Теперь разберем, как будет выглядеть запрос для поиска элементов с максимальным количеством совпадений:
ВЫБРАТЬ
ЗначенияОбъектов.Объект КАК Ссылка,
КОЛИЧЕСТВО(ЗначенияОбъектов.Значение) КАК КоличествоСовпадений
ПОМЕСТИТЬ ВТ_Результаты
ИЗ
РегистрСведений.ЗначенияОбъектов КАК ЗначенияОбъектов
ГДЕ
ЗначенияОбъектов.Значение В (&МассивПоиска)
СГРУППИРОВАТЬ ПО
ЗначенияОбъектов.Объект
;
ВЫБРАТЬ
ВТ_Результаты.Ссылка,
ВТ_Результаты.КоличествоСовпадений
ИЗ
ВТ_Результаты КАК ВТ_Результаты
УПОРЯДОЧИТЬ ПО
КоличествоСовпадений УБЫВ
Проанализируем этот запрос. Мы выбираем только те записи, которые содержат хотя бы одно значение из нашего списка. Группируя по объекту и подсчитывая количество строк, мы получаем точное число совпадений для каждого элемента. Сортировка по убыванию выводит наиболее подходящие объекты в начало списка. При наличии индекса на поле Значение, этот запрос отработает за доли секунды даже на миллионах строк.
Если специфика задачи позволяет небольшую задержку в обновлении индекса (обычно несколько минут), мы можем воспользоваться механизмом полнотекстового поиска 1С. Это отличный способ решить задачу «релевантности» без сложных запросов.
Рассмотрим алгоритм действий:
ИЛИ.ПолнотекстовыйПоиск.СоздатьСписок().Преимущество этого метода в том, что система 1С сама ранжирует результаты. Тот элемент, в котором встретилось больше всего искомых слов (значений), получит более высокий балл релевантности и окажется первым в выдаче. Это избавляет программиста от необходимости вручную прописывать логику подсчета весов.
Если структуру справочника со 100 реквизитами изменить нельзя (например, это типовая конфигурация на поддержке), нам придется формировать текст запроса программно. Мы можем вычислить «вес» каждого объекта прямо в теле запроса, используя конструкцию ВЫБОР.
Посмотрим на пример формирования такого запроса:
ТекстЗапроса = "ВЫБРАТЬ Ссылка, (";
Для Индекс = 1 По 100 Цикл
ИмяПоля = "Реквизит" + Индекс;
ТекстЗапроса = ТекстЗапроса + "ВЫБОР КОГДА " + ИмяПоля + " В (&НаборДанных) ТОГДА 1 ИНАЧЕ 0 КОНЕЦ";
Если Индекс < 100 Тогда
ТекстЗапроса = ТекстЗапроса + " + ";
КонецЕсли;
КонецЦикла;
ТекстЗапроса = ТекстЗапроса + ") КАК Вес ИЗ Справочник.МойСправочник ГДЕ Вес > 0 УПОРЯДОЧИТЬ ПО Вес УБЫВ";
Хотя этот метод кажется рабочим, проанализируем его слабые стороны. На больших объемах данных (580 тыс. записей) такой расчет «на лету» создаст колоссальную нагрузку на процессор СУБД, так как для каждой строки таблицы придется выполнить 100 проверок. Этот метод допустим только в случае, если поиск выполняется редко и по ограниченному подмножеству данных.
При поиске по большому количеству параметров (например, когда пользователь вводит 30-50 значений), оператор В (&Массив) начинает работать медленнее. Проанализируем способ ускорения через временные таблицы.
Вместо передачи массива в параметр, мы поместим искомые значения во временную таблицу. Посмотрим, как это изменит логику выполнения на уровне СУБД:
// 1. Создаем ВТ с искомыми данными
МенеджерВТ = Новый МенеджерВременныхТаблиц;
ЗапросВТ = Новый Запрос;
ЗапросВТ.МенеджерВременныхТаблиц = МенеджерВТ;
ЗапросВТ.Текст = "ВЫБРАТЬ Значение ПОМЕСТИТЬ ВТ_Искомое ИЗ &ТЗ_Данных КАК ТЗ";
ЗапросВТ.УстановитьПараметр("ТЗ_Данных", ТаблицаСДаннымиПользователя);
ЗапросВТ.Выполнить();
// 2. Основной поиск через соединение
Запрос.Текст =
"ВЫБРАТЬ
ЗначенияОбъектов.Объект,
КОЛИЧЕСТВО(*) КАК Вес
ИЗ
РегистрСведений.ЗначенияОбъектов КАК ЗначенияОбъектов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Искомое КАК ВТ_Искомое
ПО ЗначенияОбъектов.Значение = ВТ_Искомое.Значение
СГРУППИРОВАТЬ ПО
ЗначенияОбъектов.Объект
УПОРЯДОЧИТЬ ПО
Вес УБЫВ";
Использование ВНУТРЕННЕЕ СОЕДИНЕНИЕ с временной таблицей позволяет SQL-серверу применить алгоритмы Hash Join или Merge Join. Это на порядки быстрее, чем многократные проверки условий в блоке ГДЕ. Именно такой подход является промышленным стандартом при работе с высоконагруженными системами на платформе 1С.
Подведем итог и выберем стратегию в зависимости от условий:
Разберем последний важный момент: интерфейс пользователя. Вместо 100 полей на форме, пользователю удобнее работать с динамическим списком или табличной частью, где он может добавлять только нужные признаки. Это упрощает не только ввод данных, но и их последующую обработку в запросах, так как система будет оперировать только заполненными значениями.