В процессе разработки на платформе 1С часто возникает задача управления порядковыми номерами строк в таблице значений (ТЗ). Типовая ситуация: пользователь добавляет и удаляет строки, при этом важно сохранять привязку конкретного номера к существующей строке. Если из последовательности 1, 2, 3, 4, 5 была удалена строка №3, то при добавлении новой записи система должна присвоить ей именно номер 3, а не 6. В этой статье мы подробно разберем несколько алгоритмов реализации такого «умного» нумератора — от простых до высокопроизводительных.
Начнем с самого очевидного способа, который подходит для небольших таблиц (до нескольких сотен строк). Суть заключается в том, что мы запускаем цикл от единицы до общего количества строк и для каждого числа проверяем, существует ли оно уже в колонке с номерами. Как только мы находим число, которого нет в таблице, — это и есть наш первый свободный номер.
Проанализируем пример кода, реализующий этот подход:
Функция ПолучитьНовыйНомерГруппы(ТЗГруппы)
// Выгружаем колонку, чтобы ускорить поиск, или ищем прямо в ТЗ
Для Счетчик = 1 По ТЗГруппы.Количество() + 1 Цикл
НайденнаяСтрока = ТЗГруппы.Найти(Счетчик, "НомерГруппы");
Если НайденнаяСтрока = Неопределено Тогда
Возврат Счетчик;
КонецЕсли;
КонецЦикла;
Возврат 1; // Если таблица пуста
КонецФункции
Важный нюанс: Обратите внимание, что цикл идет до ТЗГруппы.Количество() + 1. Это необходимо для случая, когда в последовательности нет пропусков — тогда новый номер будет следующим по порядку после максимального существующего.
Несмотря на простоту, у этого метода есть существенный недостаток — производительность. На каждой итерации цикла вызывается метод Найти(), который сам по себе выполняет перебор таблицы. В худшем случае сложность алгоритма составит O(n²), что станет заметно на таблицах с тысячами строк.
Рассмотрим более изящное решение, которое предложили участники обсуждения. Вместо того чтобы каждый раз искать «дырки» в таблице при добавлении строки, мы можем запоминать номера в момент их удаления. Для этого используется вспомогательный массив или список значений, работающий по принципу стека.
Разберем алгоритм по шагам:
СтекУдаленныхНомеров (тип Массив).СтекУдаленныхНомеров.Добавить(УдаляемыйНомер).Поп), удаляем его из массива и присваиваем новой строке.МаксимальныйНомер + 1.Этот метод работает мгновенно (сложность O(1)), так как нам не нужно перебирать таблицу. Однако он требует дисциплины: вы должны гарантировать, что все удаления строк проходят через вашу процедуру, фиксирующую освободившиеся номера.
Если нам необходимо найти пропущенный номер в уже существующей таблице, где стек не велся, мы можем оптимизировать «тупой» цикл из первого метода. Для этого сначала отсортируем таблицу по колонке номера.
Выясним причину эффективности этого способа: после сортировки нам достаточно одного прохода по таблице, чтобы найти первый числовой разрыв.
Функция НайтиПервыйСвободныйНомерОптимизировано(ТЗ)
КопияТЗ = ТЗ.Скопировать();
КопияТЗ.Сортировать("НомерГруппы Возр");
ОжидаемыйНомер = 1;
Для Каждого СтрокаТЗ Из КопияТЗ Цикл
Если СтрокаТЗ.НомерГруппы > ОжидаемыйНомер Тогда
// Мы нашли дырку!
Прервать;
ИначеЕсли СтрокаТЗ.НомерГруппы = ОжидаемыйНомер Тогда
ОжидаемыйНомер = ОжидаемыйНомер + 1;
КонецЕсли;
КонецЦикла;
Возврат ОжидаемыйНомер;
КонецФункции
В данном случае сложность определяется скоростью сортировки (O(n log n)), что значительно быстрее, чем поиск в цикле для каждой строки.
Для тех, кто хочет добиться максимальной скорости поиска без сортировки, отлично подойдет объект Соответствие. Поиск в нем работает на основе хеш-таблиц и практически не зависит от объема данных.
Рассмотрим реализацию:
Соответствие.Истина).Получить().
ХешНомеров = Новый Соответствие;
Для Каждого Стр Из ТЗ Цикл
ХешНомеров.Вставить(Стр.НомерГруппы, Истина);
КонецЦикла;
СвободныйНомер = 1;
Пока ХешНомеров.Получить(СвободныйНомер) <> Неопределено Цикл
СвободныйНомер = СвободныйНомер + 1;
КонецЦикла;
Этот метод сочетает в себе простоту кода и высокую скорость работы на больших массивах данных.
В среде 1С использование механизмов запросов часто является признаком хорошего тона, особенно если данных действительно много. Мы можем вычислить первый свободный номер, используя левое соединение таблицы чисел с нашей таблицей номеров.
Проанализируем логику запроса:
ВТ_СуществующиеНомера.Номенклатура или ФизическиеЛица), используя функцию АВТОНОМЕРЗАПИСИ() или выбрав поле ССЫЛКА и ограничив количество.ЛЕВОЕ СОЕДИНЕНИЕ таблицы-последовательности с ВТ_СуществующиеНомера.ВТ_СуществующиеНомера равно NULL — там и находится пропущенный номер. Выбираем МИНИМУМ из таких номеров.Этот способ кажется избыточным для простых экранных форм, но он незаменим, если вы работаете с данными в объектной модели базы данных и хотите избежать блокировок или лишних пересылок данных между сервером и клиентом.
Подводя итог, выберем оптимальный вариант в зависимости от условий:
Важное предостережение: Если ваша обработка работает в многопользовательском режиме и записывает данные в общую базу, помните о транзакционных блокировках. Два пользователя могут одновременно вычислить, что номер «7» свободен, и попытаться занять его. В таких случаях лучше доверить нумерацию стандартным механизмам автопрефиксации 1С или использовать объект БлокировкаДанных.