Одной из наиболее критичных ошибок при разработке на платформе 1С является выполнение запросов внутри цикла. Если у вас есть документ или обработка с табличной частью из 100 или 1000 строк, и для каждой строки вызывается функция с запросом к базе данных, это приводит к катастрофическому падению производительности. Система вынуждена сотни раз устанавливать соединение с СУБД, передавать параметры и получать данные, что создает лишнюю нагрузку на сеть и сервер приложений.
В данной статье мы разберем, как правильно переработать алгоритм проверки остатков (конкретно для задачи определения «последнего товара по сети»), чтобы он работал быстро и эффективно вне зависимости от количества строк в документе.
Рассмотрим исходную ситуацию. Нам нужно для каждой строки табличной части проверить два условия:
Если оба условия истины, мы ставим флаг ПоследнийПоСкладам в значение Истина. При выполнении этого действия в цикле Для каждого Стр из Объект.Товары программа каждый раз «ходит» на сервер и в базу данных. Мы проанализируем, как перенести эту логику на сервер и выполнить все проверки одним пакетным запросом.
Для начала нам нужно получить список всех товаров, которые требуют проверки. Вместо того чтобы обрабатывать их по одному, мы выгрузим их в массив. Разберем, как это сделать правильно. Важно помнить, что в табличной части номенклатура может повторяться, поэтому перед запросом желательно оставить только уникальные значения, чтобы не перегружать СУБД лишней работой.
Посмотрим на пример подготовки данных на стороне сервера:
&НаСервере
Процедура ЗаполнитьКолонкуПоследнийПоСкладамНаСервере()
// Получаем массив уникальных ссылок на номенклатуру из табличной части
ТаблицаТоваров = Объект.Товары.Выгрузить();
ТаблицаТоваров.Свернуть("Номенклатура");
МассивНоменклатуры = ТаблицаТоваров.ВыгрузитьКолонку("Номенклатура");
// Если таблица пустая, делать нечего
Если МассивНоменклатуры.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Далее мы перейдем к созданию запроса
КонецПроцедуры
Теперь проанализируем структуру запроса. Вместо того чтобы делать два разных обращения к регистру ТоварыНаСкладах.Остатки, мы объединим их в одном пакете. Главное правило оптимизации здесь — использование параметров виртуальных таблиц. Фильтры по номенклатуре и складу должны находиться внутри скобок Остатки(), а не в секции ГДЕ. Это позволяет платформе 1С подготовить максимально эффективный SQL-запрос, который выберет только нужные записи.
Разберем конструкцию запроса по шагам:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК ОстатокНаСкладе
|ПОМЕСТИТЬ ВТ_ОстаткиНаСкладе
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (&МассивНоменклатуры) И Склад = &Склад) КАК ТоварыНаСкладахОстатки
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК ОстатокПоСети
|ПОМЕСТИТЬ ВТ_ОстаткиПоСети
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (&МассивНоменклатуры)) КАК ТоварыНаСкладахОстатки
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ОстаткиПоСети.Номенклатура КАК Номенклатура,
| ВЫБОР
| КОГДА ЕСТЬNULL(ВТ_ОстаткиНаСкладе.ОстатокНаСкладе, 0) = 1
| И ЕСТЬNULL(ВТ_ОстаткиПоСети.ОстатокПоСети, 0) = 1
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК ЭтоПоследнийТовар
|ИЗ
| ВТ_ОстаткиПоСети КАК ВТ_ОстаткиПоСети
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ОстаткиНаСкладе КАК ВТ_ОстаткиНаСкладе
| ПО ВТ_ОстаткиПоСети.Номенклатура = ВТ_ОстаткиНаСкладе.Номенклатура";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
Запрос.УстановитьПараметр("Склад", Объект.Склад);
// Выгружаем результат в таблицу значений для удобного поиска
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
ТаблицаРезультатов.Индексы.Добавить("Номенклатура");
Обратим внимание на использование ИНДЕКСИРОВАТЬ ПО во временных таблицах. Это критически важно, когда мы соединяем таблицы в третьем пакете — индексы позволяют СУБД мгновенно сопоставить строки. Также мы использовали ЛЕВОЕ СОЕДИНЕНИЕ и функцию ЕСТЬNULL. Это гарантирует, что если товара нет на текущем складе (но он есть на других), мы получим 0, а не пустое значение (NULL), что позволит избежать ошибок при сравнении.
Выясним, как быстро перенести полученные данные обратно в документ. После того как мы получили ТаблицаРезультатов, мы можем быстро находить в ней данные для каждой строки документа. Использование индекса в ТаблицаРезультатов по полю Номенклатура делает метод НайтиСтроки() чрезвычайно быстрым.
Рассмотрим финальную часть процедуры:
// Очищаем колонку перед заполнением
Для каждого Стр из Объект.Товары Цикл
Стр.ПоследнийПоСкладам = Ложь;
КонецЦикла;
// Заполняем на основе данных из запроса
Отбор = Новый Структура("Номенклатура");
Для каждого Стр из Объект.Товары Цикл
Отбор.Номенклатура = Стр.Номенклатура;
НайденныеСтроки = ТаблицаРезультатов.НайтиСтроки(Отбор);
Если НайденныеСтроки.Количество() > 0 Тогда
Стр.ПоследнийПоСкладам = НайденныеСтроки[0].ЭтоПоследнийТовар;
КонецЕсли;
КонецЦикла;
Проанализируем еще более продвинутый способ. Можно не делать два пакета запроса к одному и тому же регистру, а выполнить один запрос к общим остаткам и посчитать остаток по конкретному складу с помощью условий в полях выбора. Это может быть эффективнее на больших объемах данных, так как СУБД прочитает таблицу остатков всего один раз.
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| СУММА(Остатки.КоличествоОстаток) КАК ОбщийОстаток,
| СУММА(ВЫБОР
| КОГДА Остатки.Склад = &Склад
| ТОГДА Остатки.КоличествоОстаток
| ИНАЧЕ 0
| КОНЕЦ) КАК ОстатокНаСкладе
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура В (&МассивНоменклатуры)) КАК Остатки
|СГРУППИРОВАТЬ ПО
| Остатки.Номенклатура";
В этом случае в коде 1С нам останется только проверить: Если Строка.ОбщийОстаток = 1 И Строка.ОстатокНаСкладе = 1 Тогда.... Такой подход минимизирует количество временных таблиц и упрощает чтение кода.
Рассмотрим несколько дополнительных советов, которые помогут сделать ваш код еще лучше:
В (&Массив) может привести к тому, что 1С выберет вообще все записи из таблицы, что «повесит» сервер.Остатки() первым параметром всегда передавайте Момент Времени документа или актуальную дату. Если оставить параметр пустым, система будет рассчитывать остатки на «сейчас», что может быть неверно при работе с документами за прошлые периоды.ПоследнийПоСкладам в табличной части имеет тип Булево. Это позволит избежать лишних преобразований типов.МенеджерВременныхТаблиц.Таким образом, мы выяснили причину медленной работы исходного кода и заменили его на современный и быстрый механизм пакетной обработки данных. Вместо N запросов (где N — количество строк) мы теперь выполняем всего один запрос к базе данных, что является золотым стандартом разработки на платформе 1С.