Как ускорить поиск по таблице значений в 1С 8.3 с помощью индексов
Содержание
Чтобы проиндексировать таблицу значений в 1С, вызовите ТаблицаЗначений.Индексы.Добавить("Колонка1, Колонка2") с теми колонками, по которым потом ищете через НайтиСтроки(). Индекс превращает поиск из перебора всех строк в быстрый выбор по ключу, и на таблице в 50–100 тысяч строк цикл с поиском ускоряется в десятки раз.
Индексы живут только в памяти, пока существует объект. Они не сохраняются в базе, не переносятся при выгрузке из запроса и требуют расхода памяти. Поэтому для высокой производительности важно не просто «повесить индекс», а подобрать его под реальный отбор и вовремя заменить таблицу на Соответствие или временную таблицу запроса, если так быстрее.
Как устроен индекс в таблице значений и когда он нужен
Без индекса НайтиСтроки() перебирает таблицу целиком: проверяет каждую строку на совпадение с отбором. Один поиск по 10 000 строк незаметен. Но если вызвать его в цикле по другой таблице на те же 10 000 строк, получится до 100 млн сравнений. Так и появляются обработки, которые «висят» по полчаса.
Индекс — это служебная упорядоченная структура по значениям выбранных колонок. Платформа строит её один раз и дальше поддерживает сама: при добавлении, удалении строк и изменении значений в индексированных колонках.
Индекс оправдан, когда:
- 🔁 поиск по таблице идёт многократно, обычно в цикле;
- 📊 в таблице тысячи строк и больше;
- 🎯 отбор каждый раз идёт по одному и тому же набору колонок.
Индекс не нужен, когда:
- 🐢 поиск выполняется один-два раза — построение индекса само по себе стоит примерно как несколько полных переборов;
- 📝 таблица маленькая, на десятки или сотни строк;
- ✏️ индексированные колонки часто меняются в цикле — каждое изменение заставляет платформу перестраивать индекс по этой строке.
Индекс ускоряет поиск, но не сортировку и не выгрузку. Сортировать(), Свернуть(), Итог() работают так же, как без индекса. Не ждите от него ускорения там, где нет поиска.
Создание индекса программно: метод Индексы.Добавить
У каждой таблицы значений есть свойство Индексы — коллекция индексов. Главный метод — Добавить(). В него передаётся строка с именами колонок через запятую.
ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗ.Колонки.Добавить("Склад", Новый ОписаниеТипов("СправочникСсылка.Склады"));
ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
// Простой индекс по одной колонке
ТЗ.Индексы.Добавить("Номенклатура");
// Составной индекс по двум колонкам
ТЗ.Индексы.Добавить("Номенклатура, Склад");
После этого поиск пишется обычным образом:
Отбор = Новый Структура("Номенклатура, Склад", ТоварСсылка, СкладСсылка);
НайденныеСтроки = ТЗ.НайтиСтроки(Отбор);
Для Каждого Строка Из НайденныеСтроки Цикл
Строка.Количество = Строка.Количество + 1;
КонецЦикла;
НайтиСтроки() возвращает массив строк таблицы. Если ничего не найдено, массив пустой, а не Неопределено, поэтому проверяйте НайденныеСтроки.Количество() = 0.
Составной индекс: что учесть
Составной индекс строится по набору колонок. Надёжнее всего, когда ключи структуры отбора в НайтиСтроки() совпадают с колонками индекса один в один.
| Отбор в НайтиСтроки | Индекс, который стоит создать |
|---|---|
Номенклатура | "Номенклатура" |
Номенклатура, Склад | "Номенклатура, Склад" |
Номенклатура, Склад, Серия | "Номенклатура, Склад, Серия" |
В разных местах кода и Номенклатура, и Номенклатура, Склад | два индекса: по каждому набору |
Не стоит строить «один большой индекс на все случаи». Если отбор идёт только по части колонок, нет гарантии, что платформа применит индекс именно так, как вы ожидаете. Проверяйте замером.
Когда создавать индекс: до или после заполнения
Оба варианта рабочие. Разница в том, кто платит за построение:
- 📥 Индекс до заполнения. Каждая
ТЗ.Добавить()сразу встраивает строку в индекс. Удобно, если таблица растёт по ходу алгоритма и в нём же идёт поиск. - 📦 Индекс после заполнения. Индекс строится один раз по готовым данным. Обычно это выгоднее для таблиц, полученных из запроса через
Выгрузить(), когда дальше идёт только чтение.
Управление коллекцией индексов: проверка, удаление, перестройка
Коллекция Индексы — обычный объект, с ним можно работать как с другими коллекциями: узнать количество, удалить ненужный индекс, очистить все.
// Сохраняем ссылку на индекс, чтобы потом удалить именно его
ИндексПоТовару = ТЗ.Индексы.Добавить("Номенклатура");
// ... массовое изменение колонки Номенклатура ...
ТЗ.Индексы.Удалить(ИндексПоТовару); // убрать один индекс
ТЗ.Индексы.Очистить(); // убрать все индексы
КоличествоИндексов = ТЗ.Индексы.Количество();
Зачем это нужно на практике:
- Массовая правка ключевой колонки. Если в цикле по всей таблице вы переписываете значение индексированной колонки, платформа перестраивает индекс на каждой строке. Быстрее удалить индекс, сделать правку и добавить индекс заново.
- Разные этапы алгоритма. На первом этапе ищете по
Контрагент, на втором — поДоговор. Держать оба индекса всё время — лишняя память и лишняя работа при каждом изменении. Удалите первый, когда он больше не нужен. - Передача таблицы дальше. Если таблица уходит в другую процедуру, где поиск идёт по другим колонкам, там удобно очистить индексы и построить свои.
Индексы не переносятся «магически» на новые объекты. Таблица из РезультатЗапроса.Выгрузить() приходит без индексов. Если таблицу копировали через Скопировать() или получили из другого модуля, не рассчитывайте на индексы исходника — создайте нужные явно.
Соответствие вместо поиска по таблице
Частый приём практика: если нужно найти одну строку по одному ключу, вместо индекса заводят Соответствие. Доступ по ключу в нём быстрее, чем любой поиск по таблице, и код короче.
СтрокиПоТовару = Новый Соответствие;
Для Каждого Строка Из ТЗ Цикл
СтрокиПоТовару.Вставить(Строка.Номенклатура, Строка);
КонецЦикла;
НайденнаяСтрока = СтрокиПоТовару.Получить(ТоварСсылка);
Если НайденнаяСтрока <> Неопределено Тогда
НайденнаяСтрока.Количество = НайденнаяСтрока.Количество + 1;
КонецЕсли;
Если ключ составной, его можно склеить в строку, но для ссылок это ненадёжно и медленно. В таком случае проще остаться на таблице с составным индексом.
Как ускорить работу с таблицей значений через запрос
Иногда индекс в памяти — не лучший путь. Если задача — соединить две таблицы, сгруппировать или отобрать по условию, быстрее отдать это СУБД. Таблицу значений передают в запрос параметром и помещают во временную таблицу. Там же её индексируют конструкцией ИНДЕКСИРОВАТЬ ПО.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.УстановитьПараметр("Товары", ТЗ);
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Склад КАК Склад,
| Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &Товары КАК Т
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура,
| Склад
|;
|
|ВЫБРАТЬ
| ВТ.Номенклатура,
| ВТ.Склад,
| ВТ.Количество,
| Остатки.КоличествоОстаток КАК Остаток
|ИЗ
| ВТ_Товары КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(
| ,
| (Номенклатура, Склад) В
| (ВЫБРАТЬ ВТ2.Номенклатура, ВТ2.Склад ИЗ ВТ_Товары КАК ВТ2)) КАК Остатки
| ПО ВТ.Номенклатура = Остатки.Номенклатура
| И ВТ.Склад = Остатки.Склад";
Результат = Запрос.Выполнить().Выгрузить();
Регистр в примере — условный, подставьте свой. Что здесь важно:
- 🧱 Типизация колонок. У колонок таблицы значений должны быть заданы типы через
ОписаниеТипов. Колонка без типа в запросе вызывает ошибку при выполнении — это самая частая причина, почему «таблица не передаётся в запрос». - 🗂 Таблица-параметр — только источник временной таблицы. Из
&Товарывыбирают вПОМЕСТИТЬ, а соединяют уже временную таблицу. - ⚡ ИНДЕКСИРОВАТЬ ПО — по полям соединения и отбора. На маленьких временных таблицах индекс может не дать выигрыша, на десятках тысяч строк он заметен.
- 🧹 Отбор в параметрах виртуальной таблицы, а не в
ГДЕ. Так СУБД считает остатки только по нужным товарам.
Правило выбора простое. Нужно много точечных поисков в алгоритме на встроенном языке — индекс в таблице значений или Соответствие. Нужно соединить данные с регистрами или справочниками — временная таблица с ИНДЕКСИРОВАТЬ ПО.
Как измерить выигрыш от индексации
Без замера легко «оптимизировать» то, что не тормозило. Есть два способа.
Быстрый замер в коде. Функция ТекущаяУниверсальнаяДатаВМиллисекундах() возвращает время в миллисекундах. Засеките его до и после участка:
Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
Для Каждого СтрокаДокумента Из ТаблицаДокумента Цикл
Отбор = Новый Структура("Номенклатура, Склад",
СтрокаДокумента.Номенклатура, СтрокаДокумента.Склад);
Найдено = ТЗ.НайтиСтроки(Отбор);
КонецЦикла;
Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало;
Сообщить(СтрШаблон("Поиск занял %1 мс", Формат(Длительность, "ЧГ=0")));
Прогоните участок дважды: без строки ТЗ.Индексы.Добавить(...) и с ней. Сравнивайте на реальном объёме данных, а не на десяти тестовых строках.
Замер производительности отладчика. В Конфигураторе запустите отладку и включите замер производительности из меню Отладка. Он покажет, сколько времени и сколько вызовов пришлось на каждую строку модуля. Строка с НайтиСтроки() в цикле, которая забирает 90% времени, — прямой кандидат на индекс.
Ориентир по ожиданиям. На таблице примерно в 50 000 строк поиск в цикле по 50 000 строк документа без индекса считает миллиарды сравнений. С индексом каждый поиск укладывается в доли миллисекунды, а весь цикл — в секунды. Точные цифры зависят от сервера, типов колонок и режима (клиент или сервер), поэтому мерьте у себя.
Частые ошибки при индексации таблицы значений
Ещё одна ловушка — поиск по колонке с составным типом или с Неопределено. Если в колонке вперемешку пустые ссылки, Неопределено и NULL из запроса, отбор по пустому значению находит не те строки, которые вы ожидаете. Это не про индекс, но выглядит как «индекс работает неправильно». Приведите пустые значения к одному виду ещё в запросе, например через ЕСТЬNULL.
Сравнение способов: индекс, Соответствие, запрос
| Способ | Когда подходит | Что учесть |
|---|---|---|
| Поиск без индекса | Таблица до сотен строк, один-два поиска | Растёт квадратично в циклах |
Индексы.Добавить() + НайтиСтроки() | Многократный поиск по одному набору колонок, нужно несколько строк результата | Память, перестройка при изменении ключевых колонок |
Соответствие | Один ключ → одна строка или одно значение | Составные ключи неудобны, дубли ключа перезаписываются |
Временная таблица + ИНДЕКСИРОВАТЬ ПО | Соединение с регистрами и справочниками, группировки | Нужна типизация колонок, работает только на сервере |
| Перебор с выходом по первому совпадению | Таблица заранее отсортирована, нужен один поиск | Код сложнее, выигрыш небольшой |
Частые вопросы
Сохраняется ли индекс таблицы значений между вызовами сервера?
Индексы.Количество() и при необходимости создайте индекс заново.Можно ли проиндексировать дерево значений или список значений?
Индексы у них нет. Для быстрого поиска по дереву строят вспомогательное Соответствие ключ → строка дерева. Для списка значений есть НайтиПоЗначению(), но без индекса.Сколько индексов можно создать на одной таблице?
Влияет ли тип колонки на скорость индекса?
Ускоряет ли индекс метод Скопировать() с отбором?
НайтиСтроки() по индексу и передать полученный массив в Скопировать().