Как оптимизировать запросы в 1С: избавляемся от обращений к базе данных в циклах

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

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

Почему запросы в цикле замедляют систему?

Когда мы выполняем запрос внутри цикла, каждое итерирование требует отдельного сетевого вызова от сервера приложений 1С к серверу СУБД (например, MS SQL Server или PostgreSQL). Рассмотрим ситуацию: если в цикле 1000 итераций, система выполнит 1000 отдельных TCP-пакетов, 1000 раз СУБД будет компилировать план запроса, открывать и закрывать транзакции или контексты чтения. Проанализируем затраты времени: даже если сам запрос выполняется за 1 мс, суммарные накладные расходы на сетевое взаимодействие и управление контекстом могут составить 10-20 мс на каждый вызов. В итоге операция, которая могла занять доли секунды, растягивается на 20 секунд. Для детального понимания происходящего на стороне СУБД мы рекомендуем анализировать SQL сервер глазами 1С-ника.

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


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

В данном примере при заполнении документа с сотнями строк пользователь будет наблюдать длительное зависание интерфейса. Выясним причину: вместо одного массового получения данных, мы заставляем систему "бегать" в базу данных за каждой строчкой. Наша задача — изменить этот подход.

Шаг 1: Сбор данных для фильтрации

Первым делом мы должны собрать все необходимые параметры для запроса в одну коллекцию. Обычно для этого используется объект Массив или СписокЗначений. Мы пройдем по строкам документа и выгрузим уникальные ссылки на номенклатуру, чтобы передать их в запрос одним списком. Рассмотрим, как это сделать эффективно:


СписокНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
// Удалим дубликаты, чтобы уменьшить объем передаваемых данных в СУБД
МассивНоменклатуры = Новый Массив;
ТаблицаНоменклатуры = Новый ТаблицаЗначений;
ТаблицаНоменклатуры.Колонки.Добавить("Номенклатура");
Для Каждого ТекНом Из СписокНоменклатуры Цикл
    ЗаполнитьЗначенияСвойств(ТаблицаНоменклатуры.Добавить(), Новый Структура("Номенклатура", ТекНом));
КонецЦикла;
ТаблицаНоменклатуры.Свернуть("Номенклатура");
МассивНоменклатуры = ТаблицаНоменклатуры.ВыгрузитьКолонку("Номенклатура");

Шаг 2: Выполнение единого запроса к данным

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


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

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

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

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

Шаг 3: Использование временных таблиц для больших объемов данных

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

  1. Подготовим таблицу значений с нужными колонками.
  2. Передадим её в запрос в качестве параметра.
  3. Поместим данные из параметра во временную таблицу.
  4. Проиндексируем временную таблицу по ключевым полям.
  5. Выполним соединение временной таблицы с таблицами базы данных.

Проанализируем пример кода:


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

// Передаем таблицу товаров документа (предварительно свернутую)
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаНоменклатуры);

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

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

Использование МенеджерВременныхТаблиц и оператора ПОМЕСТИТЬ позволяет нам работать с данными максимально эффективно. Индексация временной таблицы ИНДЕКСИРОВАТЬ ПО критически важна для быстрого выполнения ЛЕВОЕ СОЕДИНЕНИЕ, особенно когда строк больше нескольких сотен.

Шаг 4: Быстрый поиск в результатах запроса

После получения общего результата запроса нам нужно распределить данные обратно по строкам документа. Выполнять поиск методом НайтиСледующий() в выборке для каждой строки — тоже не самый быстрый вариант. Оптимальным способом является использование объекта Соответствие (Map), где ключом будет ссылка на номенклатуру, а значением — цена. Проанализируем, как это реализовать:


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

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

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

Метод Получить() у объекта Соответствие работает практически мгновенно, так как использует хеш-таблицы. Это финальный штрих нашей оптимизации, который делает алгоритм линейным по сложности O(n) вместо квадратичной сложности O(n*m), которая была в самом начале.

Дополнительные рекомендации по работе с запросами

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

  1. Не используйте точку от поля ссылочного типа в запросах. Фразы типа Номенклатура.Артикул в условии запроса приводят к неявному соединению с таблицей справочника, что может быть избыточным. Старайтесь соединять таблицы явно.
  2. Минимизируйте количество колонок. Выбирайте в ВЫБРАТЬ только те поля, которые действительно будут использованы. Это снижает нагрузку на сеть и память сервера приложений.
  3. Используйте пакетные запросы. Если вам нужно получить несколько разных срезов данных, объединяйте их в один объект Запрос, разделяя тексты символом ";". Это позволяет выполнить несколько операций за одно обращение к серверу СУБД.
  4. Следите за параметрами виртуальных таблиц. Условия должны накладываться именно в параметрах виртуальной таблицы (в скобках), а не в секции ГДЕ. Это позволяет платформе 1С передать фильтры напрямую в SQL-запрос к СУБД, отсекая лишние данные на самом раннем этапе.

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

← На главную