Как правильно и быстро реализовать поиск вхождения кодов ОКПД2 по иерархии в 1С?

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

При разработке систем закупок или контроля номенклатуры в конфигурациях уровня 1С:ERP мы часто сталкиваемся с задачей фильтрации товаров по классификаторам, таким как ОКПД2. Классификатор ОКПД2 имеет иерархическую структуру, где код верхнего уровня (например, "22.29") включает в себя все вложенные коды (например, "22.29.29.190").

Рассмотрим типичную задачу: у нас есть регистр сведений с "запрещенными" кодами. В этом регистре могут храниться как полные коды конкретных товаров, так и коды групп (частичные коды). Нам необходимо проверить, попадает ли конкретная номенклатура с полным кодом под блокировку, если в "черном списке" находится этот код или любой его родитель.

Давайте разберем несколько подходов к реализации этой задачи, проанализируем их производительность и выберем наиболее эффективный алгоритм.

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

Предположим, у нас есть следующие вводные данные:

  1. Номенклатура имеет код ОКПД2: 22.29.29.190.
  2. Регистр запретов содержит запись: 22.29 (блокируется вся группа).

Нам нужно, чтобы система поняла: код 22.29.29.190 начинается с 22.29, следовательно, закупка запрещена.

Рассмотрим, какие ошибки часто допускаются при реализации такой проверки в запросе.

Подход №1: Использование оператора ПОДОБНО (Неоптимальный)

Первая мысль, которая приходит в голову разработчику — использовать оператор ПОДОБНО (аналог LIKE в SQL). Логика кажется простой: мы проверяем, похож ли код номенклатуры на код из регистра с добавлением символа процента.


ВЫБРАТЬ
    Номенклатура.Ссылка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЗапрещенныеКодыОКПД2 КАК Запрет
        ПО Номенклатура.КодОКПД2 ПОДОБНО Запрет.Код + "%"

Почему этот метод плох?

Давайте проанализируем, как это работает на уровне СУБД (MS SQL или PostgreSQL). Условие соединения ПОДОБНО Запрет.Код + "%" является динамическим. Шаблон поиска формируется "на лету" из данных таблицы регистра.

Это приводит к следующим последствиям:

  1. Невозможность использования индексов (Non-SARGable query). Оптимизатор СУБД не может использовать индекс по полю Код в регистре сведений, так как он не ищет конкретное значение.
  2. Полный перебор (Table Scan / Index Scan). Для каждой проверяемой номенклатуры СУБД будет вынуждена перебирать все записи в таблице запрещенных кодов, чтобы примерить шаблон. Если в регистре тысячи записей, а проверяете вы сотню номенклатур, количество операций возрастет до сотен тысяч (декартово произведение).

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

Подход №2: Метод "Вхождения в список родителей" (Рекомендуемый)

Чтобы проверка работала мгновенно, нам нужно использовать возможности индексирования. Для этого мы должны искать в регистре точное совпадение, а не подобие.

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

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

Шаг 1. Подготовка данных на сервере 1С

Перед выполнением запроса мы берем код ОКПД2 проверяемой номенклатуры (например, 22.29.29.190) и программно генерируем массив всех его иерархических уровней.

Используем функцию СтрРазделить для быстрого парсинга.


// Пример функции для получения массива родителей
Функция ПолучитьВариантыКодаОКПД2(Знач ПолныйКод)
    
    МассивЧастей = СтрРазделить(ПолныйКод, ".", Ложь);
    МассивВариантов = Новый Массив;
    
    ТекущаяСтрока = "";
    Для Каждого Часть Из МассивЧастей Цикл
        Если Не ПустаяСтрока(ТекущаяСтрока) Тогда
            ТекущаяСтрока = ТекущаяСтрока + ".";
        КонецЕсли;
        ТекущаяСтрока = ТекущаяСтрока + Часть;
        
        // Добавляем вариант (например "22", затем "22.29", затем "22.29.29")
        МассивВариантов.Добавить(ТекущаяСтрока);
    КонецЦикла;
    
    Возврат МассивВариантов;
    
КонецФункции

В результате для кода 22.29.29.190 мы получим массив:

Шаг 2. Выполнение запроса

Теперь мы можем передать этот массив в запрос и использовать оператор В (IN).


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Запрет.Код КАК ЗапрещенныйКод
    |ИЗ
    |   РегистрСведений.ЗапрещенныеКодыОКПД2 КАК Запрет
    |ГДЕ
    |   Запрет.Код В (&МассивВариантов)";

Запрос.УстановитьПараметр("МассивВариантов", МассивВариантов);
Результат = Запрос.Выполнить();

Преимущества этого метода:

  1. Index Seek. Оператор В со списком конкретных значений позволяет SQL-серверу точечно обращаться к индексу регистра. Это работает экстремально быстро, даже если в регистре миллионы записей.
  2. Простота. Код 1С выполняется быстро, а запрос остается примитивным.

Подход №3: Структурное хранение (Вариант из форума)

В обсуждении (сообщения 8 и 9) был предложен интересный вариант изменения структуры хранения данных. Суть его заключается в том, чтобы хранить части кода ОКПД2 в отдельных измерениях регистра сведений.

Давайте посмотрим, как это может выглядеть. Вместо одной строки Код, мы создаем измерения:

При записи в регистр код "22.29" запишется как: Уровень1="22", Уровень2="29", остальные пустые.

Анализ решения:

Хотя этот метод позволяет структурировать данные, он делает запрос сложнее и менее гибким. Вам придется писать громоздкие условия ИЛИ, проверяя совпадение по разному количеству заполненных уровней. В сравнении с методом №2 (поиск по списку родителей), этот метод требует серьезной переработки метаданных конфигурации, но не дает существенного выигрыша в скорости чтения.

Подход №4: Денормализация (Кеширование признака)

Если ваша система работает под высокой нагрузкой (например, высокочастотная торговля или тысячи строк в заказе), имеет смысл рассмотреть вариант денормализации данных.

Суть метода: перенести вычисления с момента чтения (проверки документа) на момент записи.

  1. Добавляем в справочник Номенклатура реквизит булево типа ЗапрещенПоОКПД2.
  2. Создаем подписку на событие при записи набора записей регистра запретов. Когда пользователь добавляет новый запрещенный код (например, "22.29"), система запускает фоновое задание.
  3. Фоновое задание ищет всю номенклатуру, у которой код начинается на "22.29", и устанавливает флаг ЗапрещенПоОКПД2 = Истина.
  4. Аналогичную проверку делаем при создании новой номенклатуры.

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


Если Номенклатура.ЗапрещенПоОКПД2 Тогда
    Отказ = Истина;
    Сообщить("Товар запрещен к закупке");
КонецЕсли;

Это самое быстрое решение для оперативной работы, но оно требует аккуратной реализации механизмов обновления этого признака.

Выводы

Подведем итоги. Для реализации поиска по части кода ОКПД2 наиболее эффективными являются следующие решения:

  1. Для запросов и отчетов: Используйте метод "Список родителей" (Подход №2). Разложите код номенклатуры на составляющие (родительские группы) и используйте оператор В к регистру запретов. Это гарантирует использование индексов СУБД.
  2. Для высоконагруженных операций: Используйте кеширование признака (Подход №4) в карточке номенклатуры.
  3. Чего избегать: Не используйте конструкцию ПОДОБНО Регистр.Код + "%" в соединениях таблиц, так как это приведет к полному сканированию таблиц и падению производительности.
← На главную