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

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

Одной из самых распространенных и критических ошибок при разработке на платформе 1С:Предприятие является выполнение запроса внутри цикла. Эта антипаттерн-практика способна замедлить работу системы в десятки и сотни раз, особенно когда количество итераций (строк в документе или элементов в списке) растет. В данной статье мы разберем, почему так происходит, и рассмотрим правильные методы оптимизации кода на конкретных примерах.

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

Давайте проанализируем ситуацию: каждый раз, когда вызывается метод Запрос.Выполнить(), система выполняет целый ряд тяжеловесных операций:

  1. Сетевой обмен: Клиентское приложение отправляет команду серверу приложений, а тот, в свою очередь, обращается к СУБД (SQL Server, PostgreSQL и др.).
  2. Компиляция плана: СУБД анализирует текст запроса и строит оптимальный план его выполнения. Если запрос в цикле, СУБД делает это снова и снова.
  3. Блокировки: Частые обращения к таблицам могут создавать лишние ожидания на уровне базы данных.

Если в табличной части вашего документа 100 строк, то вместо одного эффективного обращения к базе вы делаете 100 «мелких» вызовов, каждый из которых несет накладные расходы.

Разбор исходной проблемы

Рассмотрим код, который часто встречается у начинающих разработчиков:


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

Здесь основная проблема в том, что запрос выполняется для каждой строки Объект.Товары. Наша задача — получить все данные за один вызов сервера.

Метод 1: Использование массива параметров

Самый простой способ исправить ситуацию — это собрать все ссылки на номенклатуру в массив и передать его в запрос. Рассмотрим этот алгоритм по шагам:

  1. Создадим массив и заполним его уникальными ссылками на номенклатуру из табличной части.
  2. Настроим запрос так, чтобы он выбирал данные сразу по всему списку.
  3. Выгрузим результат запроса в таблицу значений для быстрого поиска в памяти.

Проанализируем пример реализации:


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

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

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

// 3. Выполняем и выгружаем в таблицу для поиска
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
// Для ускорения поиска можно добавить индекс
ТаблицаРезультат.Индексы.Добавить("Номенклатура,Характеристика");

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

Метод 2: Временные таблицы (Золотой стандарт)

Если в табличной части много строк или требуется соединение данных ТЧ с несколькими регистрами, лучше использовать Временные Таблицы (ВТ). Это позволяет передать всю структуру документа в СУБД и выполнить соединение на стороне сервера базы данных.

Рассмотрим, как это работает:


// 1. Выгружаем ТЧ в Таблицу Значений
ТЗ_Товары = Объект.Товары.Выгрузить();

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

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

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

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

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

Использование временных таблиц считается наиболее правильным подходом, так как ВНУТРЕННЕЕ СОЕДИНЕНИЕ на уровне SQL работает гораздо быстрее, чем ручной поиск методом Найти() в коде 1С.

Важные замечания по оптимизации

Проанализируем еще несколько моментов, которые помогут сделать ваш код безупречным:

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

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

← На главную