Как быстро пронумеровать строки в таблице значений

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При работе с большими объемами данных в 1С:Предприятии часто возникает задача нумерации строк в объекте ТаблицаЗначений. Казалось бы, задача тривиальная — достаточно пройтись циклом и присвоить значения. Однако, когда количество строк исчисляется сотнями тысяч или миллионами, выбор алгоритма начинает критически влиять на общую производительность системы. В этой статье мы совместно разберем различные подходы, проанализируем их скорость и определим абсолютного чемпиона по производительности.

Зачем вообще нумеровать строки в таблице значений?

Прежде чем мы перейдем к коду, давайте выясним причины. Хотя у строк таблицы значений есть системные индексы, они отражают лишь текущее расположение записи в памяти. Стоит нам отсортировать таблицу — и прежний порядок будет утерян. Нумерация позволяет зафиксировать последовательность, создать «место в рейтинге» или обеспечить корректную связь записей при сложных математических расчетах. Рассмотрим ситуацию: нам нужно рассчитать средний рейтинг объектов после нескольких различных сортировок. Без жесткой фиксации номера строки сделать это эффективно практически невозможно.

Способ 1. Классические циклы

Начнем с самого очевидного решения — перебора коллекции. Проанализируем несколько вариантов реализации цикла. Многие начинающие разработчики используют конструкцию Для Каждого, однако замеры показывают, что она не всегда является оптимальной для данной задачи.

Рассмотрим вариант с использованием индекса:


Для ит = 0 По ТЗ.Количество() - 1 Цикл
    ТЗ[ит].НомерСтроки = ит + 1;
КонецЦикла;

А теперь сравним его с циклом по коллекции:


Номер = 1;
Для Каждого СтрокаТЗ Из ТЗ Цикл
    СтрокаТЗ.НомерСтроки = Номер;
    Номер = Номер + 1;
КонецЦикла;

Важный нюанс: как показала практика и обсуждения экспертов, обращение к строке по индексу ТЗ[ит] внутри цикла выполняется чуть медленнее, чем прямой перебор коллекции Для Каждого, из-за дополнительных затрат на поиск объекта в памяти по его порядковому номеру. Но даже эти методы меркнут на фоне специализированных функций платформы.

Способ 2. Победитель по скорости — ЗагрузитьКолонку()

Экспериментальным путем было доказано, что самым быстрым способом является подготовка данных во внешней коллекции (массиве) с последующей массовой загрузкой в таблицу. Метод ЗагрузитьКолонку() реализован на уровне платформы крайне эффективно, так как он минимизирует количество обращений интерпретатора к объектам базы данных.

Разберем по шагам этот алгоритм:

  1. Создаем массив нужного размера.
  2. Заполняем его числами от 1 до N.
  3. Используем метод ЗагрузитьКолонку() для моментальной вставки данных.

Посмотрим на пример реализации, который показал лучшие результаты на тестах с 1 000 000 строк:


Колво = ТЗ.Количество();
МассивНомеров = Новый Массив(Колво); // Сразу выделяем память под нужный размер
Для ит = 0 По Колво - 1 Цикл
    МассивНомеров[ит] = ит + 1;
КонецЦикла;
ТЗ.ЗагрузитьКолонку(МассивНомеров, "НомерСтроки");

Почему это работает быстрее? Дело в том, что когда мы меняем значение в каждой строке ТаблицыЗначений напрямую в цикле, платформа выполняет множество сервисных операций (проверка типов, уведомление связанных элементов управления и т.д.) на каждой итерации. Метод ЗагрузитьКолонку выполняет «инъекцию» данных целым блоком, что значительно сокращает накладные расходы.

Способ 3. Микрооптимизация заполнения массива

Даже внутри самого быстрого способа есть пространство для маневра. Выясним, как заполнить сам массив еще быстрее. Существует разница между использованием метода Добавить() и обращением по заранее определенному индексу.

Проанализируем два варианта заполнения массива:

Вариант А (медленнее):


Массив = Новый Массив;
Для ит = 1 По 1000000 Цикл
    Массив.Добавить(ит);
КонецЦикла;

Вариант Б (быстрее):


Массив = Новый Массив(1000000);
Для ит = 0 По 999999 Цикл
    Массив[ит] = ит + 1;
КонецЦикла;

В варианте Б мы исключаем необходимость динамического расширения памяти массива на каждой итерации, что дает существенный прирост на сверхбольших объемах данных.

Способ 4. Использование порождающих запросов

В некоторых случаях нумерацию можно переложить на плечи SQL-сервера. Этот метод интересен тем, что мы можем получить готовую пронумерованную таблицу еще на этапе формирования данных. Рассмотрим концепцию «запроса-генератора», который создает последовательность чисел без использования реальных таблиц базы данных.

Пример формирования последовательности через декартово произведение временных таблиц в запросе:


// Пример генерации чисел от 0 до 255 через соединения
ТекстЗапроса = "ВЫБРАТЬ 0 КАК Х ПОМЕСТИТЬ Регистр1 ОБЪЕДИНИТЬ ВЫБРАТЬ 1;
|////////////////////////////////////////////////////////////////
|ВЫБРАТЬ Младшие.Х + 2 * Старшие.Х КАК Х ПОМЕСТИТЬ Регистр2 
|ИЗ Регистр1 КАК Младшие, Регистр1 КАК Старшие;
|////////////////////////////////////////////////////////////////
|ВЫБРАТЬ Младшие.Х + 4 * Старшие.Х КАК Х ИЗ Регистр2 КАК Младшие, Регистр2 КАК Старшие";

Хотя этот метод кажется элегантным, замеры на реальных задачах показывают, что для простой нумерации существующей ТаблицыЗначений метод с ЗагрузитьКолонку() все равно остается лидером, обгоняя запросы почти в три раза на объемах в 5 миллионов строк. Запросы целесообразно использовать только в том случае, если нумерация является частью более сложной выборки данных.

Способ 5. Нестандартный подход через Табличные части

Существует еще один «хитрый» способ, который подходит для определенных конфигураций. Если мы загрузим ТаблицуЗначений в ТабличнуюЧасть любого объекта или обработки (у которых есть системное поле НомерСтроки), платформа автоматически пронумерует строки силами внутреннего движка, написанного на низкоуровневых языках. Затем данные можно выгрузить обратно.


НЗ = РегистрыНакопления.МойРегистр.СоздатьНаборЗаписей();
НЗ.Загрузить(ТЗ); // Здесь происходит автоматическая нумерация системного поля НомерСтроки
МассивНомеров = НЗ.ВыгрузитьКолонку("НомерСтроки");
ТЗ.ЗагрузитьКолонку(МассивНомеров, "Номер");

Этот метод может быть удивительно быстрым, но он сильно зависит от структуры метаданных и наличия подходящих объектов под рукой.

Подведем итоги

Проанализировав все доступные варианты, мы можем составить иерархию эффективности:

  1. Абсолютный лидер: Создание массива с предварительным выделением памяти и использование ЗагрузитьКолонку().
  2. Второе место: Прямой цикл Для Каждого с использованием локальной переменной-счетчика (для небольших таблиц до 10 000 строк разница будет незаметна).
  3. Третье место: Использование запросов и временных таблиц (эффективно только при получении данных, но не при обработке готовой ТЗ).
  4. Аутсайдер: Цикл с обращением по индексу ТЗ[ит] и многократным вызовом ТЗ.Количество() в условии цикла.

Важный совет: При использовании метода с массивом всегда типизируйте колонку в таблице значений как Число. Это позволит платформе работать с данными напрямую, минуя лишние проверки типов Произвольный, что добавит еще несколько драгоценных миллисекунд к вашей производительности.

← На главную