В процессе разработки часто возникает задача управления порядком элементов внутри групп иерархического справочника. Типичное требование: элементы в пределах одного родителя должны быть пронумерованы строго по порядку (1, 2, 3... N). Это позволяет пользователям менять элементы местами, например, устанавливая элементу позицию 2.5, чтобы он встал между вторым и третьим, после чего система должна автоматически нормализовать все индексы обратно в целочисленные. Рассмотрим, как реализовать алгоритм проверки и исправления такого порядка максимально эффективно.
Представим справочник, где у каждого элемента есть числовой реквизит Позиция. Наша цель — убедиться, что для каждого родителя набор значений в этом поле представляет собой непрерывную последовательность натуральных чисел от 1 до количества элементов в группе. Если порядок нарушен (например, появились дробные числа, дубли или пропуски), система должна это обнаружить и пересчитать индексы. Проанализируем основные подходы к решению этой задачи.
Это наиболее изящный и производительный способ поиска "проблемных" групп без использования ресурсоемких соединений (JOIN) в запросах. Идея основана на формуле суммы членов арифметической прогрессии. Если в группе N элементов, то сумма их позиций должна быть равна N * (N + 1) / 2. Если сумма отличается — в группе точно нарушен порядок.
Разберем реализацию этого алгоритма на языке запросов 1С:
ВЫБРАТЬ
Т.Родитель КАК Родитель,
КОЛИЧЕСТВО(*) КАК Количество,
СУММА(Т.Позиция) КАК СуммаПозиции
ПОМЕСТИТЬ ТГруппы
ИЗ
Справочник.ВашСправочник КАК Т
СГРУППИРОВАТЬ ПО
Т.Родитель
ИМЕЮЩИЕ
(1 + КОЛИЧЕСТВО(*)) * КОЛИЧЕСТВО(*) / 2 <> СУММА(Т.Позиция)
Однако, как справедливо заметили участники обсуждения, одной проверки суммы недостаточно, так как возможны коллизии (например, набор [1, 1.5, 3.5] даст ту же сумму, что и [1, 2, 3]). Рассмотрим подробнее, как усилить эту проверку без потери производительности. Для полной гарантии правильности ряда 1..N необходимо добавить дополнительные условия в ИМЕЮЩИЕ:
МИНИМУМ(Позиция) = 1МАКСИМУМ(Позиция) = КОЛИЧЕСТВО(*)КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Позиция) = КОЛИЧЕСТВО(*)С учетом этих дополнений запрос становится практически неуязвимым для ложноположительных срабатываний и при этом отрабатывает мгновенно даже на больших объемах данных.
Если математический метод кажется слишком абстрактным, рассмотрим более традиционный способ — сравнение текущей позиции с "идеальным" номером строки, полученным через соединение таблицы самой с собой. Мы можем посчитать, сколько элементов в этой же группе имеют позицию меньше или равную текущей.
Пример запроса для выявления конкретных элементов, требующих изменения:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
КОЛИЧЕСТВО(Номенклатура1.Ссылка) КАК НоваяПозиция
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Номенклатура1
ПО Номенклатура.Родитель = Номенклатура1.Родитель
И (Номенклатура.Позиция > Номенклатура1.Позиция
ИЛИ (Номенклатура.Позиция = Номенклатура1.Позиция
И Номенклатура.Ссылка >= Номенклатура1.Ссылка))
СГРУППИРОВАТЬ ПО
Номенклатура.Родитель,
Номенклатура.Ссылка,
Номенклатура.Позиция
ИМЕЮЩИЕ
КОЛИЧЕСТВО(Номенклатура1.Ссылка) <> Номенклатура.Позиция
Преимущество: Этот запрос сразу выдает готовую новую позицию для каждого элемента. Недостаток: На справочниках в десятки тысяч элементов такое соединение N*N будет работать значительно медленнее математического метода.
В современных версиях платформы 1С 8.3 появилась поддержка оконных функций. Это позволяет решить задачу максимально "чисто" с точки зрения SQL. Мы можем рассчитать порядковый номер элемента в контексте его родителя, упорядочив данные по текущему полю позиции.
Хотя в языке запросов 1С синтаксис оконных функций ограничен, логика выглядит так: мы выбираем данные, применяем ПОРЯДКОВЫЙ_НОМЕР() с группировкой по Родителю и сортировкой по Позиции. Если полученное значение не совпадает с записанным в базе — элемент нуждается в обновлении.
Выясним, как эффективно обновить данные после того, как "проблемные" группы найдены. Рассмотрим программный код, который обходит выборку по группировкам и перезаписывает только изменившиеся элементы:
// Выборка уже содержит только те группы, где сумма прогрессии не сошлась
ВыборкаГрупп = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаГрупп.Следующий() Цикл
// Внутри каждой группы элементы уже упорядочены по текущей (возможно дробной) позиции
ВыборкаЭлементов = ВыборкаГрупп.Выбрать();
НовыйИндекс = 0;
Пока ВыборкаЭлементов.Следующий() Цикл
НовыйИндекс = НовыйИндекс + 1;
// Оптимизация: записываем только если значение действительно изменилось
Если ВыборкаЭлементов.Позиция <> НовыйИндекс Тогда
ОбъектЭлемента = ВыборкаЭлементов.Ссылка.ПолучитьОбъект();
ОбъектЭлемента.Позиция = НовыйИндекс;
// Используем ОбменДанными.Загрузка = Истина, если не нужны проверки при записи
ОбъектЭлемента.Записать();
КонецЕсли;
КонецЦикла;
КонецЦикла;
Проанализируем ситуацию, когда элементов в группе очень много и частая перезапись всех позиций (как в случае с 1, 2, 3...) создает очереди блокировок. В таких системах применяется алгоритм LexoRank. Вместо чисел используются строки (например, "a", "b", "c").
В среде 1С такой подход оправдан в высоконагруженных системах (например, в списках задач крупных проектов), где интерактивность важнее идеальной красоты индексов в базе данных.
Для большинства типовых задач в 1С оптимальным будет сочетание математического поиска в запросе и точечного обновления объектов. Рассмотрим финальный чек-лист внедрения:
ВЫРАЗИТЬ(Позиция КАК Число(15,0)) = Позиция), если в вашей системе разрешен ввод дробных чисел для сортировки.ПередЗаписью, которая будет автоматически предлагать "красивый" номер позиции.Таким образом, мы получили надежный и быстрый механизм поддержания иерархического порядка, сочетающий в себе мощь математики и возможности платформы 1С.