Как оптимизировать медленный запрос к справочнику Номенклатура в 1С 7.7?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

При работе с большими справочниками в 1С:Предприятии 7.7 (содержащими от 100 000 до сотен тысяч записей) разработчики нередко сталкиваются с катастрофическим падением производительности встроенного языка запросов. Простейший запрос по отбору номенклатуры может выполняться несколько минут даже в терминальном режиме на производительном сервере. Разберем внутренние механизмы платформы 7.7, выясним истинные причины медленной работы и рассмотрим эффективные варианты ускорения выборки данных.

Почему встроенный запрос в 1С 7.7 выполняется долго

Проанализируем типовую конструкцию запроса, вызывающую зависание:


//{{ЗАПРОС(Сформировать)
|Обрабатывать НеПомеченныеНаУдаление;
|Без итогов;
|Номенклатура = Справочник.Номенклатура.ТекущийЭлемент;
|Группировка Номенклатура без групп;
|Условие(Номенклатура в Отбор);
|"//}}ЗАПРОС

Медленная работа этой конструкции обусловлена тремя фундаментальными особенностями движка запросов платформы 1С 7.7:

  1. Игнорирование индексов в секции Условие: Встроенный интерпретатор запросов 1С 7.7 не использует индексные файлы (.cdx) для вычисления условий фильтрации. Платформа всегда производит последовательное сканирование всей таблицы справочника от первой до последней записи (Full Scan). При объеме в 200 000 позиций это означает чтение колоссального объема данных с диска.
  2. Рекурсивный поиск иерархии оператором в: Конструкция Номенклатура в Отбор при проверке принадлежности группе не просто сравнивает значения полей. Для каждого проверяемого элемента платформа начинает программно подниматься вверх по цепочке родителей до самого корня дерева (аналогично вызову метода ПринадлежитЭлементу()). В результате при 200 000 записях генерируются сотни тысяч случайных обращений к файлу базы данных.
  3. Накладные расходы на временные таблицы: Встроенный движок запросов 1С 7.7 формирует результат во временных файлах в каталоге пользователя (%TEMP%), создавая собственные промежуточные файлы и строя по ним временные индексы, что многократно увеличивает дисковую нагрузку.

Именно поэтому стандартный перебор через ВыбратьЭлементы() с проверкой ПринадлежитЭлементу(Отбор) внутри цикла работает точно так же медленно: алгоритмическая сложность и количество дисковых чтений остаются идентичными.

Решение 1. Разделение логики отбора штатными средствами (без сторонних библиотек)

Если требуется остаться в рамках штатных средств платформы без модификации конфигурации сторонними компонентами, разберем структуру поля «Отбор». Пользователь может указать либо конкретный элемент, либо целую группу (папку). Проанализируем эти сценарии отдельно.

Случай А: Выбран конкретный элемент

Если в поле отбора указан конкретный элемент (Отбор.ЭтоГруппа() = 0), выполнять запрос или перебор справочника вообще не имеет смысла. Товар уже известен, и его можно сразу передавать в отчет или алгоритм обработки. Попытка прогнать весь справочник через запрос ради одного элемента — частая ошибка, которая и приводит к 3-минутным задержкам.

Случай Б: Выбрана группа справочника

Если в отборе указана группа (Отбор.ЭтоГруппа() = 1), категорически нельзя обходить справочник «сверху вниз» от корня. Вместо встроенного запроса применим специализированную выборку по родителю с использованием индекса через методы ИспользоватьРодителя() и ВключатьПодчиненные():


Если ПустоеЗначение(Отбор) = 1 Тогда
    // Если отбор не задан, обходим справочник без иерархии
    Спр = СоздатьОбъект("Справочник.Номенклатура");
    Спр.ВыбратьЭлементы(0); // 0 отключает иерархический обход, ускоряя чтение
    Пока Спр.ПолучитьЭлемент() = 1 Цикл
        Если (Спр.ЭтоГруппа() = 0) И (Спр.ПометкаУдаления() = 0) Тогда
            // Обработка элемента
        КонецЕсли;
    КонецЦикла;
ИначеЕсли Отбор.ЭтоГруппа() = 0 Тогда
    // Выбран единичный элемент - обход не требуется
    Если Отбор.ПометкаУдаления() = 0 Тогда
        // Обработка одного элемента: Отбор
    КонецЕсли;
Иначе
    // Выбрана группа: задействуем выборку только подчиненных узлов по индексу
    Спр = СоздатьОбъект("Справочник.Номенклатура");
    Спр.ИспользоватьРодителя(Отбор);
    Спр.ВключатьПодчиненные(1); // Рекурсивно выбирает элементы всех вложенных папок
    Спр.ВыбратьЭлементы();
    Пока Спр.ПолучитьЭлемент() = 1 Цикл
        Если (Спр.ЭтоГруппа() = 0) И (Спр.ПометкаУдаления() = 0) Тогда
            // Обработка элемента нужной ветки
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

Метод ИспользоватьРодителя() использует системный индекс по полю родителя и считывает с диска исключительно записи, принадлежащие выбранной иерархической ветке. Время получения данных сокращается с 3–4 минут до долей секунды.

Решение 2. Использование прямых запросов (компонента 1С++)

Если логика отчета требует сложного соединения данных (например, номенклатуры, цен, остатков или бухгалтерских итогов), наилучшим решением для 1С 7.7 является переход на прямые SQL-запросы с помощью бесплатной внешней компоненты 1С++.

Важное преимущество: технология прямых запросов одинаково эффективно работает как на SQL-версиях 1С 7.7, так и на файловых базах DBF (через встроенный провайдер данных SQLite). Прямой запрос обходит медленный транслятор платформы и напрямую обращается к физическим таблицам базы данных с использованием всех преимуществ индексирования.

Посмотрим на пример формирования прямого запроса с передачей списка значений отбора во временную таблицу:


БазаДанных = СоздатьОбъект("ODBCData");
БазаДанных.Соединение();

ЗапросSQL = СоздатьОбъект("ODBCRecordset");
ЗапросSQL.УстБД(БазаДанных);

// Подготавливаем виртуальную таблицу фильтрации
Если ПустоеЗначение(Отбор) = 0 Тогда
    ЗапросSQL.УложитьСписокОбъектов(Отбор, "#ВыбОтбор", "Справочник.Номенклатура");
КонецЕсли;

ТекстЗапроса = "
|SELECT 
|    Спр.ID [Номенклатура $Справочник.Номенклатура]
|FROM 
|    $Справочник.Номенклатура AS Спр
|WHERE 
|    Спр.ISMARK = 0
|    AND Спр.ISFOLDER = 2";

Если ПустоеЗначение(Отбор) = 0 Тогда
    ТекстЗапроса = ТекстЗапроса + "
    |    AND Спр.ID IN (SELECT Val FROM #ВыбОтбор)";
КонецЕсли;

ТЗ = ЗапросSQL.ВыполнитьИнструкцию(ТекстЗапроса);

Разберем, что делает этот код:

  1. Метод УложитьСписокОбъектов() автоматически раскладывает переданный элемент, группу или целый список значений во временную таблицу базы данных (включая разворачивание групп по иерархии).
  2. Конструкция $Справочник.Номенклатура позволяет метапарсеру 1С++ автоматически сопоставить внутренние имена таблиц и полей 1С с физическими именами в файлах DBF или таблицах Microsoft SQL Server.
  3. Поля ISMARK = 0 и ISFOLDER = 2 исключают помеченные на удаление элементы и папки на уровне движка базы данных, минуя интерпретатор 1С.

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

Рекомендации по системной оптимизации

Если база работает в файловом режиме (DBF) в терминале, выполним следующие настройки системного окружения:

  1. Исключения антивируса: Обязательно добавим каталог базы данных, а также папку временных файлов пользователей (%TEMP%) в исключения постоянного мониторинга антивирусного ПО. Антивирусная проверка каждого считываемого блока DBF/CDX способна замедлить выполнение запроса в 3–5 раз.
  2. Каталог временных файлов на RAM-диске: Перенос каталога %TEMP% терминальных сессий на виртуальный диск в оперативной памяти (RAM-диск) радикально ускоряет построение любых штатных отчетов и запросов в среде 1С 7.7.
← На главную