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

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

Одной из наиболее распространенных и критических ошибок начинающих разработчиков 1С является выполнение запроса внутри цикла. На небольших объемах данных такая конструкция может работать незаметно, но при росте базы данных она неизбежно приводит к катастрофическому падению производительности. В этой статье мы разберем, почему так происходит, и рассмотрим «человеческие» способы решения этой задачи, предложенные опытными разработчиками.

В чем заключается проблема «запроса в цикле»?

Рассмотрим классический антипаттерн, с которого началась дискуссия. Программист обходит строки таблицы товаров и для каждой строки отправляет отдельный запрос к базе данных, чтобы получить остаток. Проблема здесь в том, что каждый вызов Запрос.Выполнить() порождает отдельное обращение сервера приложений к серверу СУБД. Если в таблице 500 строк, система сделает 500 «походов» в базу данных. Большая часть времени при этом тратится не на получение данных, а на сетевые задержки и построение планов выполнения запросов на стороне SQL-сервера.

Решение №1: Использование оператора «В» и списка значений

Самый простой способ оптимизации — собрать все необходимые объекты (в нашем случае Номенклатура) в один массив или список и передать их в запрос одним параметром. Проанализируем, как это реализовать:


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

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

Запрос.УстановитьПараметр("СписокНоменклатуры", СписокНоменклатуры);
РезультатЗапроса = Запрос.Выполнить();

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

Решение №2: Использование временных таблиц и соединений (Профессиональный подход)

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

  1. Выгружаем исходную таблицу (ТЧ документа или ТЗ) в запрос.
  2. Помещаем её во временную таблицу в базе данных.
  3. Выполняем ЛЕВОЕ СОЕДИНЕНИЕ временной таблицы с виртуальной таблицей остатков.

Проанализируем пример кода, реализующий этот подход:


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

// Передаем таблицу значений в запрос. Важно: колонки в ТЗ должны быть типизированы!
Запрос.УстановитьПараметр("ТаблицаТоваров", Товары.Выгрузить());

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

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

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

Использование функции ЕСТЬNULL крайне важно при левом соединении. Если товара нет на остатке, запрос вернет NULL, а функция преобразует его в 0, что позволит избежать ошибок при записи в числовые поля.

Как эффективно сопоставить результат запроса с исходной таблицей?

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

Важные технические нюансы

Разберем еще несколько советов, которые помогут сделать ваш код по-настоящему надежным:

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

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

3. Типизация колонок: При передаче таблицы значений в запрос через параметр убедитесь, что у колонок ТЗ заданы типы (через ОписаниеТипов). Если колонка имеет неопределенный тип, СУБД не сможет построить эффективный план запроса.

Выясним причину, по которой многие продолжают писать запросы в циклах: часто это кажется «проще» в реализации «здесь и сейчас». Однако переход на пакетную обработку данных — это признак профессионального роста разработчика 1С. Рассмотренные нами методы использования временных таблиц и фильтрации в параметрах виртуальных таблиц являются «золотым стандартом» разработки, обеспечивающим высокую скорость работы системы даже при больших нагрузках.

← На главную