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

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

При разработке сложных обработок и документов в 1С часто возникает необходимость связать несколько табличных частей, рассчитать нарастающий итог, распределить материалы или сопоставить строки с остатками на складах. Если выполнять подобные операции некорректно — через передачу таблиц значений в запросы внутри циклов, — интерфейс начинает зависать, а сервер СУБД испытывает огромную паразитную нагрузку. Разберем по шагам, почему возникает эта проблема и как кардинально ускорить работу алгоритмов.

В чем причина медленной работы запросов с таблицами значений?

Проанализируем типовую ситуацию: в коде формы при изменении строки вызывается серверная функция, куда передаются выгруженные таблицы значений, помещаются во временные таблицы (ПОМЕСТИТЬ ВТ...), соединяются и возвращаются назад. Если такая функция вызывается для каждой строки в цикле, время выполнения возрастает в геометрической прогрессии.

Выделим основные технические причины снижения быстродействия:

  1. Накладные расходы СУБД на DDL и DML-операции: При каждом вызове запроса с конструкцией ПОМЕСТИТЬ ВТ ИЗ &ТаблицаЗначений платформа 1С отправляет на сторону СУБД (PostgreSQL или MS SQL) команды на создание физической временной таблицы (например, pg_temp.tt...), выполняет вставку строк (INSERT) и последующее удаление. Создание и наполнение временных таблиц в цикле заставляет СУБД непрерывно блокировать системные каталоги и совершать дисковые операции.
  2. Нетипизированные поля: Если колонки таблицы значений не имеют жесткого ОписаниеТипов, СУБД создает поля с универсальными или бинарными типами (например, BYTEA, MVARCHAR), что блокирует оптимизацию по индексам.
  3. Лишние обращения через точку: Использование обращений к полям реквизитов через точку в тексте запроса (например, ТаблицаМатериалов.НоменклатураМатериал.ЕдиницаИзмерения) порождает неявные левые соединения (LEFT JOIN) с реальными таблицами базы данных, утяжеляя план выполнения.
  4. Неэффективное использование Менеджера временных таблиц: Создание объекта МенеджерВременныхТаблиц оправдано только тогда, когда созданная временная таблица используется в последующих раздельных запросах. Если запрос один — создание и привязка МВТ создают лишние накладные расходы.

Способ 1. Отказ от запросов в пользу коллекций и индексов в оперативной памяти

Если все исходные данные уже получены и находятся в оперативной памяти сервера приложений или на клиенте, использовать механизм запросов СУБД для простых математических операций и фильтрации не нужно. Обработка данных в памяти 1С происходит в десятки раз быстрее.

Посмотрим на пример оптимизации поиска. По умолчанию метод НайтиСтроки() выполняет полный перебор ($O(N)$). Чтобы сделать поиск мгновенным ($O(1)$), достаточно добавить индекс к объекту ТаблицаЗначений:


// Создаем индекс по ключевым полям для ускорения поиска
ТаблицаОстатков.Индексы.Добавить("НоменклатураМатериал, ХарактеристикаМатериал");

ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("НоменклатураМатериал", СтрокаРасход.НоменклатураМатериал);
ПараметрыОтбора.Вставить("ХарактеристикаМатериал", СтрокаРасход.ХарактеристикаМатериал);

// Поиск по проиндексированной таблице работает моментально
НайденныеСтроки = ТаблицаОстатков.НайтиСтроки(ПараметрыОтбора);

Для сопоставления один к одному также эффективно использовать объект Соответствие, где ключом выступает ссылка на номенклатуру (или структура), а значением — остаток или строка данных.

Способ 2. Линейный расчет нарастающего итога за один проход

Часто в запросах пытаются реализовать расчет использованного количества с условием РасходМатериалов.НомерСтроки < &НомерСтроки. Такое соединение «само на себя» имеет квадратичную сложность ($O(N^2)$).

Вместо этого разберем, как выполнить расчет нарастающего итога в памяти в один проход ($O(N)$):


// Хранилище накопленного расхода по позициям
НакопленныйРасход = Новый Соответствие;

Для Каждого СтрокаРасход Из ТабличнаяЧастьРасход Цикл
    КлючПозиции = Новый Структура("Номенклатура, Характеристика", 
        СтрокаРасход.НоменклатураМатериал, 
        СтрокаРасход.ХарактеристикаМатериал);
        
    УжеИспользовано = НакопленныйРасход[КлючПозиции];
    Если УжеИспользовано = Неопределено Тогда
        УжеИспользовано = 0;
    КонецЕсли;
    
    // Выполняем расчет с учетом ранее накопленного расхода
    СтрокаРасход.РасходДоТекущейСтроки = УжеИспользовано;
    
    // Накапливаем итог для следующих строк
    НакопленныйРасход.Вставить(КлючПозиции, УжеИспользовано + СтрокаРасход.КоличествоИспользуемое);
КонецЦикла;

Способ 3. Пакетная выборка данных одним запросом

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

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

  1. Выгружаем уникальные пары Номенклатура и Характеристика из табличной части в массив или типизированную таблицу значений.
  2. Передаем массив в параметры виртуальной таблицы остатков регистра (например, РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Номенклатура В (&МассивНоменклатуры))).
  3. За один запрос получаем все остатки и выгружаем их в проиндексированную ТаблицуЗначений или Соответствие.
  4. Распределяем остатки по строкам на стороне сервера приложений или клиента без дополнительных обращений к СУБД.

Способ 4. Перенос интерактивных пересчетов на клиент

При интерактивной работе пользователя (например, при редактировании колонок количества, состава комплектов в форме обработки) выполнение серверного вызова на каждое изменение строки создает микрозадержки интерфейса. Перенос базовых математических расчетов (перемножение, сложение, вычисление дельты) на сторону клиента полностью устраняет задержки, сокращая время отклика с нескольких секунд до десятков миллисекунд.

← На главную