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