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

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

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

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

В чем заключается проблема?

Давайте проанализируем ситуацию, когда разработчик пишет код следующим образом: есть таблица значений или выборка, и для каждой строки этой выборки создается и выполняется новый запрос к базе данных.

Рассмотрим пример "плохого" кода, который мы часто встречаем:


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

Давайте выясним причину, почему этот код неэффективен. При каждой итерации цикла происходит обращение к СУБД (Microsoft SQL Server, PostgreSQL и др.). Если в вашей таблице 10 строк, система сделает 10 запросов. Это не критично. Но если строк 1000 или 10000?

Основные факторы замедления:

  1. Накладные расходы на вызов: Инициализация объекта Запрос, компиляция текста запроса платформой, передача данных на сервер СУБД и получение ответа занимают время.
  2. Нагрузка на СУБД: База данных вынуждена тысячи раз строить план выполнения запроса для простейшей операции.
  3. Сетевой трафик: Множество мелких пакетов передаются между сервером 1С и SQL-сервером.

Посмотрим на способы решения этой проблемы. Основное правило оптимизации: "Получай все данные одним запросом".

Способ 1: Использование списка значений и оператора "В"

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

Разберем по шагам, как это реализовать:

Сначала подготовим данные. Нам нужно пройтись по исходной коллекции и собрать ссылки в массив.


// 1. Собираем ссылки в массив
МассивСсылок = Новый Массив;
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
    МассивСсылок.Добавить(СтрокаТЧ.Номенклатура);
КонецЦикла;

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


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

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

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

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


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

Преимущества: Простой и понятный код, идеально подходит для небольших и средних объемов данных.

Недостатки: Если массив содержит десятки тысяч элементов, условие В (...) может работать медленно или превысить ограничения СУБД.

Способ 2: Использование Менеджера временных таблиц

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

Рассмотрим пример реализации с использованием пакетного запроса.


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

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

Запрос.УстановитьПараметр("ТЧ_Товары", Объект.Товары.Выгрузить());
Запрос.Выполнить(); // Выполняем первую часть пакета

Теперь, когда наши данные находятся в "облаке" временных таблиц на сервере СУБД, мы можем сделать к ним запрос и соединить их с реальными таблицами базы данных.


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

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

Пока Выборка.Следующий() Цикл
    // Обрабатываем готовые данные
КонецЦикла;

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

Способ 3: Левое соединение (если исходные данные — это тоже выборка из БД)

Часто бывает, что мы перебираем результат одного запроса и внутри цикла делаем второй запрос. В этом случае правильнее всего объединить их в один большой запрос, используя ЛЕВОЕ СОЕДИНЕНИЕ.

Представим ситуацию: мы выбираем заказы, а потом в цикле ищем их статусы. Вместо этого сделаем так:


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

При использовании ЛЕВОЕ СОЕДИНЕНИЕ важно помнить про возможные дубли строк, если в присоединяемой таблице одному ключу соответствует несколько записей (отношение "один ко многим"). В таких случаях используйте группировку или вложенные запросы с МАКСИМУМ / МИНИМУМ.

Сводные рекомендации по оптимизации

Подводя итог нашему анализу, сформулируем чек-лист для проверки вашего кода:

  1. Проверьте код на наличие конструкции Запрос.Выполнить() внутри циклов Для Каждого или Пока.
  2. Если объем данных небольшой (до 100-200 элементов), используйте метод с передачей массива в параметр и условием В (&Массив).
  3. Если работа идет с большими табличными частями или требуется сложная логика получения остатков/цен, используйте МенеджерВременныхТаблиц и пакетные запросы.
  4. Старайтесь максимально использовать возможности языка запросов (соединения, объединения), чтобы получать готовый результат "одним куском", а не собирать его постобработкой в коде 1С.
  5. Используйте Индексировать() для таблиц значений перед поиском, если вы все-таки используете метод поиска в памяти (Способ 1).

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

← На главную