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

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

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

В данной статье мы разберем, как правильно переработать алгоритм проверки остатков (конкретно для задачи определения «последнего товара по сети»), чтобы он работал быстро и эффективно вне зависимости от количества строк в документе.

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

Рассмотрим исходную ситуацию. Нам нужно для каждой строки табличной части проверить два условия:

  1. Равен ли остаток товара на текущем складе единице.
  2. Равен ли общий остаток этого же товара по всем складам (по всей «сети») единице.

Если оба условия истины, мы ставим флаг ПоследнийПоСкладам в значение Истина. При выполнении этого действия в цикле Для каждого Стр из Объект.Товары программа каждый раз «ходит» на сервер и в базу данных. Мы проанализируем, как перенести эту логику на сервер и выполнить все проверки одним пакетным запросом.

Шаг 1: Сбор данных и подготовка массива номенклатуры

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

Посмотрим на пример подготовки данных на стороне сервера:


&НаСервере
Процедура ЗаполнитьКолонкуПоследнийПоСкладамНаСервере()
    
    // Получаем массив уникальных ссылок на номенклатуру из табличной части
    ТаблицаТоваров = Объект.Товары.Выгрузить();
    ТаблицаТоваров.Свернуть("Номенклатура");
    МассивНоменклатуры = ТаблицаТоваров.ВыгрузитьКолонку("Номенклатура");
    
    // Если таблица пустая, делать нечего
    Если МассивНоменклатуры.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    // Далее мы перейдем к созданию запроса
КонецПроцедуры

Шаг 2: Создание оптимизированного пакетного запроса

Теперь проанализируем структуру запроса. Вместо того чтобы делать два разных обращения к регистру ТоварыНаСкладах.Остатки, мы объединим их в одном пакете. Главное правило оптимизации здесь — использование параметров виртуальных таблиц. Фильтры по номенклатуре и складу должны находиться внутри скобок Остатки(), а не в секции ГДЕ. Это позволяет платформе 1С подготовить максимально эффективный SQL-запрос, который выберет только нужные записи.

Разберем конструкцию запроса по шагам:

  1. Первый пакет: Получаем остатки по конкретному складу для всего списка номенклатуры.
  2. Второй пакет: Получаем общие остатки по всем складам для того же списка.
  3. Третий пакет: Соединяем эти данные и вычисляем результат проверки.

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

Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Запрос.УстановитьПараметр("Склад", Объект.Склад);

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

Обратим внимание на использование ИНДЕКСИРОВАТЬ ПО во временных таблицах. Это критически важно, когда мы соединяем таблицы в третьем пакете — индексы позволяют СУБД мгновенно сопоставить строки. Также мы использовали ЛЕВОЕ СОЕДИНЕНИЕ и функцию ЕСТЬNULL. Это гарантирует, что если товара нет на текущем складе (но он есть на других), мы получим 0, а не пустое значение (NULL), что позволит избежать ошибок при сравнении.

Шаг 3: Сопоставление результата с табличной частью

Выясним, как быстро перенести полученные данные обратно в документ. После того как мы получили ТаблицаРезультатов, мы можем быстро находить в ней данные для каждой строки документа. Использование индекса в ТаблицаРезультатов по полю Номенклатура делает метод НайтиСтроки() чрезвычайно быстрым.

Рассмотрим финальную часть процедуры:


// Очищаем колонку перед заполнением
Для каждого Стр из Объект.Товары Цикл
    Стр.ПоследнийПоСкладам = Ложь;
КонецЦикла;

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

Альтернативный вариант: Использование агрегатов в одном запросе

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


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

В этом случае в коде 1С нам останется только проверить: Если Строка.ОбщийОстаток = 1 И Строка.ОстатокНаСкладе = 1 Тогда.... Такой подход минимизирует количество временных таблиц и упрощает чтение кода.

Важные рекомендации по оптимизации

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

Таким образом, мы выяснили причину медленной работы исходного кода и заменили его на современный и быстрый механизм пакетной обработки данных. Вместо N запросов (где N — количество строк) мы теперь выполняем всего один запрос к базе данных, что является золотым стандартом разработки на платформе 1С.

← На главную