Каждый разработчик 1С рано или поздно сталкивается с оптимизацией запросов и кода. Вопрос производительности базы данных напрямую связан с правильным использованием индексов. Начинающие программисты, а также специалисты, перешедшие с платформы 1С:Предприятие 7.7, часто задаются вопросом: обязательно ли индексировать реквизит справочника (или другого объекта), если по нему планируется выполнять поиск с помощью метода НайтиПоРеквизиту()? В этой статье мы подробно разберем, как устроен этот механизм изнутри, почему старые привычки могут вводить в заблуждение и как платформа 1С:Предприятие 8.х взаимодействует с СУБД под капотом.
Для начала проанализируем, откуда берется убеждение об обязательности индексирования. В старой платформе 1С:Предприятие 7.7 метод НайтиПоРеквизиту() имел жесткое системное ограничение. Если у реквизита справочника в конфигураторе не было установлено свойство «Сортировка» (которое инициировало создание индекса на уровне базы данных), попытка вызвать метод приводила к ошибке времени выполнения. Программа просто отказывалась выполнять поиск без физического индекса.
С переходом на платформу 1С:Предприятие 8 ситуация в корне изменилась. Разработчики платформы сняли это жесткое ограничение. В современных версиях платформы (8.2, 8.3 и новее) метод НайтиПоРеквизиту() успешно отработает для любого реквизита, независимо от того, установлено ли для него свойство «Индексировать» в значение «Индексировать» или оставлено значение по умолчанию «Не индексировать».
Именно поэтому у многих разработчиков возникает когнитивный диссонанс: код компилируется, поиск выполняется, ошибок нет, хотя индекс отсутствует. Выясним причину такого поведения платформы.
Часто разработчики путают поведение метода НайтиПоРеквизиту() с другим популярным объектным методом — Выбрать(). Давайте посмотрим на синтаксис метода выборки у менеджера справочника:
Выборка = Справочники.Номенклатура.Выбрать(Родитель, Владелец, Отбор, Порядок);
Если мы попытаемся использовать параметр Отбор для метода Выбрать(), передав туда структуру с неотсортированным (непроиндексированным) реквизитом, платформа незамедлительно выбросит исключение вида: «Недопустимое значение параметра (параметр номер '3')». Это происходит потому, что метод Выбрать() с отбором на уровне платформы жестко требует наличия индекса для всех полей, входящих в отбор (за исключением стандартных полей, таких как Родитель или Владелец, которые индексируются платформой автоматически).
Таким образом, требование об обязательном индексировании реквизитов для отборов действительно существует в 1С 8.х, но относится оно к методу Выбрать(), а не к НайтиПоРеквизиту().
Рассмотрим детально, что происходит на уровне СУБД (например, MS SQL Server или PostgreSQL), когда мы вызываем метод НайтиПоРеквизиту(). Пусть у нас есть справочник Контрагенты и мы ищем элемент по реквизиту ИНН:
НайденнаяСсылка = Справочники.Контрагенты.НайтиПоРеквизиту("ИНН", "7701010101");
Платформа 1С транслирует этот вызов в SQL-запрос к СУБД. Запрос будет выглядеть примерно следующим образом:
SELECT TOP 1
_IDRRef
FROM
_Reference105
WHERE
_Fld1205 = '7701010101'
С точки зрения СУБД, этот запрос абсолютно валиден и выполним. База данных обязана вернуть результат независимо от наличия индексов. Однако способ получения этого результата будет кардинально различаться в зависимости от настроек реквизита в Конфигураторе:
_Fld1205. Произойдет операция Index Seek (поиск по индексу), которая выполняется практически мгновенно, так как база данных сразу перейдет к нужной ветке бинарного дерева индекса.Проанализируем разницу в производительности этих двух подходов. На маленькой базе данных или на справочнике, содержащем до пары сотен элементов, разницу во времени выполнения между полным сканированием таблицы и точечным поиском по индексу заметить невозможно — оба запроса отработают за доли миллисекунды. СУБД может даже намеренно проигнорировать существующий индекс на сверхмалых таблицах, посчитав чтение всей таблицы в оперативную память более дешевой операцией. Именно с этим эффектом столкнулся автор темы на форуме.
Однако если справочник разрастется до сотен тысяч или миллионов записей (например, справочники Номенклатура, Штрихкоды или Контрагенты в крупных базах данных), отсутствие индекса приведет к катастрофическому падению производительности. Каждый вызов НайтиПоРеквизиту() будет нагружать дисковую подсистему и процессор сервера СУБД чтением огромного объема данных с диска в оперативную память. Более того, при полном сканировании СУБД может накладывать блокировки на всю таблицу, что парализует параллельную работу пользователей в системе.
Разберем ситуации, когда индекс по реквизиту может быть создан платформой неявно, даже если разработчик установил свойство реквизита в значение «Не индексировать»:
Ссылка, Код, Наименование, Родитель, Владелец, автоматически индексируются платформой при создании объекта метаданных. Поиск по ним всегда выполняется быстро. Например, методы НайтиПоКоду() или НайтиПоНаименованию() всегда используют предопределенные индексы СУБД.КритерийОтбора, платформа 1С принудительно создаст индекс для этого реквизита в СУБД, игнорируя настройки самого реквизита в Конфигураторе. Это сделано для быстрого построения связанных списков.ПолнотекстовыйПоиск.Найти()) и никак не помогают СУБД при стандартном SQL-запросе, генерируемом методом НайтиПоРеквизиту().В современной практике разработки на платформе 1С:Предприятие 8 использование методов типа НайтиПоРеквизиту(), НайтиПоКоду() или НайтиПоНаименованию() в критичных участках кода считается нерекомендуемым. Посмотрим, почему использование явных запросов предпочтительнее:
1. Борьба с проблемой N+1 запросов при обработке массивов данных.
Представим сценарий загрузки данных из внешнего файла (например, Excel), где для каждой строки файла нам нужно найти контрагента по ИНН. Если мы будем использовать НайтиПоРеквизиту() в цикле, мы породим сотни или тысячи отдельных SQL-запросов к серверу СУБД:
Для Каждого СтрокаДанных Из ТаблицаИмпорта Цикл
СсылкаКонтрагента = Справочники.Контрагенты.НайтиПоРеквизиту("ИНН", СтрокаДанных.ИНН);
// Обработка данных
КонецЦикла;
Каждое обращение к базе данных тратит время на сетевой обмен между сервером приложений 1С и сервером СУБД, компиляцию плана запроса и транзакционные издержки. Гораздо эффективнее собрать все ИНН в массив и выполнить один-единственный запрос с использованием конструкции ГДЕ ИНН В (&МассивИНН):
МассивИНН = ТаблицаИмпорта.ВыгрузитьКолонку("ИНН");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Ссылка,
| Контрагенты.ИНН КАК ИНН
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.ИНН В(&МассивИНН)";
Запрос.УстановитьПараметр("МассивИНН", МассивИНН);
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
Такой подход сокращает время выполнения операции в десятки и сотни раз.
2. Проблема неявного поведения при наличии дублей.
Метод НайтиПоРеквизиту() всегда возвращает только одну ссылку, даже если в базе данных существует несколько элементов с одинаковым значением реквизита. Какую именно ссылку вернет метод — гарантировать невозможно. Это зависит от физического порядка записей в таблице или индексе СУБД, который может измениться после реиндексации или обновления статистики. Использование же объекта Запрос позволяет разработчику явно управлять результатом: выбрать все дублирующиеся элементы, отсортировать их по определенному правилу или использовать агрегатные функции.
Подведем итог нашему исследованию:
НайтиПоРеквизиту() в 1С 8.х не требует обязательного индексирования реквизита для успешного выполнения программы (в отличие от 1С 7.7).Запрос вместо стандартных методов менеджера справочника.