Как ускорить работу 1С и избавиться от запросов в цикле?

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

В этой статье мы подробно разберем одну из самых распространенных причин медленной работы обработок и документов в 1С — выполнение запросов внутри цикла. Мы проанализируем, почему так делать нельзя, и вместе перепишем код так, чтобы он работал в десятки раз быстрее.

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

Почему запросы в цикле — это плохо?

Давайте проанализируем механику работы платформы 1С. Когда мы используем конструкцию Запрос.Выполнить(), происходит обращение сервера 1С к серверу баз данных (SQL). Это "дорогая" операция, требующая времени на сетевое взаимодействие, компиляцию плана запроса и передачу данных.

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

Анализ исходной (неправильной) ситуации

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

Неэффективный код:


Для Каждого СтрокаТЧ Из Объект.Товары Цикл
    
    // Создаем новый запрос на каждой итерации - ЭТО ОШИБКА
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
        |ИЗ
        |   РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних";
    
    Запрос.УстановитьПараметр("Период", ТекущаяДата());
    Запрос.УстановитьПараметр("Номенклатура", СтрокаТЧ.Номенклатура);
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Если Выборка.Следующий() Тогда
        СтрокаТЧ.Цена = Выборка.Цена;
    КонецЕсли;
    
КонецЦикла;

Здесь мы видим классическую проблему: объект Запрос создается и уничтожается многократно. Давайте перейдем к решению.

Решение 1: Использование параметров списка (ВЫБРАТЬ ... ГДЕ ... В (&Список))

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

Шаг 1: Подготовка данных

Сначала выгрузим колонку с номенклатурой из нашей табличной части в массив. Для этого используем метод ВыгрузитьКолонку.


// Получаем массив ссылок на номенклатуру из табличной части
МассивНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");

Шаг 2: Написание пакетного запроса

Теперь напишем запрос, который будет искать цены не для одной позиции, а сразу для всех позиций из нашего массива. Обратите внимание на условие В (&МассивНоменклатуры).


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
    |   ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
    |ИЗ
    |   РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура В (&МассивНоменклатуры)) КАК ЦеныНоменклатурыСрезПоследних";

// Устанавливаем параметры один раз
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);

Заметьте, мы также добавили поле Номенклатура в выборку. Это критически важно, так как в дальнейшем нам нужно будет сопоставить полученную цену с конкретной строкой таблицы товаров.

Шаг 3: Обработка результатов и заполнение

Теперь, когда мы получили таблицу с ценами, нам нужно распределить их обратно в табличную часть. Рассмотрим два способа сделать это.

Способ А: Заполнение через поиск (для небольших таблиц)

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


// Выполняем запрос
Результат = Запрос.Выполнить();
ТаблицаЦен = Результат.Выгрузить();

// Добавляем индекс для ускорения поиска
ТаблицаЦен.Индексы.Добавить("Номенклатура");

Для Каждого СтрокаТЧ Из Объект.Товары Цикл
    
    // Ищем строку в памяти
    НайденнаяСтрока = ТаблицаЦен.Найти(СтрокаТЧ.Номенклатура, "Номенклатура");
    
    Если НайденнаяСтрока <> Неопределено Тогда
        СтрокаТЧ.Цена = НайденнаяСтрока.Цена;
    КонецЕсли;
    
КонецЦикла;

Решение 2: Использование Менеджера временных таблиц (для сложных данных)

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

Суть метода заключается в том, что мы "отправляем" нашу табличную часть внутрь запроса, соединяем её с регистром сведений прямо на сервере СУБД, и забираем готовый результат.

Шаг 1: Помещение данных во временную таблицу


Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;

// Передаем ТЧ как параметр
Запрос.УстановитьПараметр("ТЧТовары", Объект.Товары.Выгрузить());
Запрос.УстановитьПараметр("Период", ТекущаяДата());

Запрос.Текст = 
    "ВЫБРАТЬ
    |   ТЧ.Номенклатура КАК Номенклатура,
    |   ТЧ.НомерСтроки КАК НомерСтроки
    |ПОМЕСТИТЬ ВТ_Товары
    |ИЗ
    |   &ТЧТовары КАК ТЧ";
    
Запрос.Выполнить(); // Создали временную таблицу ВТ_Товары

Шаг 2: Соединение и получение результата

Теперь расширим текст запроса, добавив соединение с ценами. Используем ЛЕВОЕ СОЕДИНЕНИЕ, чтобы не потерять строки товаров, для которых цена не найдена.


Запрос.Текст = 
    "ВЫБРАТЬ
    |   ВТ_Товары.НомерСтроки КАК НомерСтроки,
    |   ЕСТЬNULL(Цены.Цена, 0) КАК Цена
    |ИЗ
    |   ВТ_Товары КАК ВТ_Товары
    |       ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Цены
    |       ПО ВТ_Товары.Номенклатура = Цены.Номенклатура
    |УПОРЯДОЧИТЬ ПО
    |   НомерСтроки";

Выборка = Запрос.Выполнить().Выбрать();

Шаг 3: Обход выборки

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


Пока Выборка.Следующий() Цикл
    // Обращаемся к строке ТЧ по индексу (НомерСтроки - 1, так как нумерация коллекций с 0)
    СтрокаТЧ = Объект.Товары[Выборка.НомерСтроки - 1];
    СтрокаТЧ.Цена = Выборка.Цена;
КонецЦикла;

Важные нюансы и рекомендации

  1. Виртуальные таблицы: При использовании параметров виртуальных таблиц (например, СрезПоследних), всегда передавайте отбор внутрь параметров виртуальной таблицы. В нашем первом примере мы написали Номенклатура В (&МассивНоменклатуры) внутри скобок виртуальной таблицы. Если бы мы написали это в секции ГДЕ, система сначала получила бы цены для всей номенклатуры базы, и только потом отфильтровала бы нужные. Это грубая ошибка производительности.
  2. Индексы: При поиске в Таблице Значений (метод Найти) обязательно добавляйте индексы через Таблица.Индексы.Добавить(), если строк больше нескольких десятков.
  3. Ограничения на NULL: При левом соединении (как во втором примере) всегда обрабатывайте возможные значения NULL с помощью функции ЕСТЬNULL, иначе при попытке записать результат в числовое поле вы получите ошибку.

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

← На главную