Одной из наиболее распространенных проблем производительности в конфигурациях на платформе 1С:Предприятие является выполнение запросов внутри циклов. Эта ошибка, часто называемая "проблемой N+1", приводит к лавинообразному росту нагрузки на сервер базы данных и значительному замедлению работы системы. Чтобы вовремя обнаружить и устранить подобные проблемы, важно регулярно проводить замер производительности контура 1С и поиск узких мест. В этой статье мы подробно разберем, почему такая архитектура кода является пагубной, проанализируем механизмы работы платформы и научимся переводить такие алгоритмы на использование массивов, временных таблиц и пакетных запросов.
Когда мы выполняем запрос внутри цикла, каждое итерирование требует отдельного сетевого вызова от сервера приложений 1С к серверу СУБД (например, MS SQL Server или PostgreSQL). Рассмотрим ситуацию: если в цикле 1000 итераций, система выполнит 1000 отдельных TCP-пакетов, 1000 раз СУБД будет компилировать план запроса, открывать и закрывать транзакции или контексты чтения. Проанализируем затраты времени: даже если сам запрос выполняется за 1 мс, суммарные накладные расходы на сетевое взаимодействие и управление контекстом могут составить 10-20 мс на каждый вызов. В итоге операция, которая могла занять доли секунды, растягивается на 20 секунд. Для детального понимания происходящего на стороне СУБД мы рекомендуем анализировать SQL сервер глазами 1С-ника.
Разберем типичный пример "плохого" кода, который часто встречается у начинающих разработчиков. Чтобы автоматически выявлять такие конструкции, можно использовать статический анализатор кода проектов:
Для Каждого СтрокаТовара Из Объект.Товары Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних";
Запрос.УстановитьПараметр("Период", Объект.Дата);
Запрос.УстановитьПараметр("Номенклатура", СтрокаТовара.Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
СтрокаТовара.Цена = Выборка.Цена;
КонецЕсли;
КонецЦикла;
В данном примере при заполнении документа с сотнями строк пользователь будет наблюдать длительное зависание интерфейса. Выясним причину: вместо одного массового получения данных, мы заставляем систему "бегать" в базу данных за каждой строчкой. Наша задача — изменить этот подход.
Первым делом мы должны собрать все необходимые параметры для запроса в одну коллекцию. Обычно для этого используется объект Массив или СписокЗначений. Мы пройдем по строкам документа и выгрузим уникальные ссылки на номенклатуру, чтобы передать их в запрос одним списком. Рассмотрим, как это сделать эффективно:
СписокНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
// Удалим дубликаты, чтобы уменьшить объем передаваемых данных в СУБД
МассивНоменклатуры = Новый Массив;
ТаблицаНоменклатуры = Новый ТаблицаЗначений;
ТаблицаНоменклатуры.Колонки.Добавить("Номенклатура");
Для Каждого ТекНом Из СписокНоменклатуры Цикл
ЗаполнитьЗначенияСвойств(ТаблицаНоменклатуры.Добавить(), Новый Структура("Номенклатура", ТекНом));
КонецЦикла;
ТаблицаНоменклатуры.Свернуть("Номенклатура");
МассивНоменклатуры = ТаблицаНоменклатуры.ВыгрузитьКолонку("Номенклатура");
Теперь, когда у нас есть массив всех товаров, мы составим один запрос, который получит цены сразу для всего списка. Процесс написания и тестирования таких конструкций значительно облегчает быстрый анализ данных. Для работы с результатами мы будем использовать виртуальную таблицу СрезПоследних, передав в параметры условие по массиву. Посмотрим на пример реализации:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура В (&МассивНоменклатуры)) КАК ЦеныНоменклатурыСрезПоследних";
Запрос.УстановитьПараметр("Период", Объект.Дата);
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
РезультатЗапроса = Запрос.Выполнить();
Важный момент: использование оператора В (&Массив) позволяет СУБД оптимизировать поиск, используя индексы. Однако стоит помнить, что если массив содержит десятки тысяч элементов, производительность может упасть. В таких случаях рекомендуется использовать временные таблицы.
Если нам нужно обработать очень большой объем данных или связать данные документа с данными базы, лучше всего передать таблицу значений целиком во временную таблицу запроса. Это позволит избежать ограничений на количество параметров в SQL-запросе и даст возможность проиндексировать данные. Для качественной отладки таких механизмов идеально подходит консоль запросов с поддержкой пакетных запросов и временных таблиц. Разберем этот процесс по шагам:
Проанализируем пример кода:
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Передаем таблицу товаров документа (предварительно свернутую)
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаНоменклатуры);
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Номенклатура
|ИЗ
| &ТаблицаТоваров КАК ТЗ
|;
|
|ИНДЕКСИРОВАТЬ ПО
| ВТ_Номенклатура.Номенклатура
|;
|
|ВЫБРАТЬ
| ВТ_Номенклатура.Номенклатура,
| Цены.Цена
|ИЗ
| ВТ_Номенклатура КАК ВТ_Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Цены
| ПО ВТ_Номенклатура.Номенклатура = Цены.Номенклатура";
РезультатЗапроса = Запрос.Выполнить();
Использование МенеджерВременныхТаблиц и оператора ПОМЕСТИТЬ позволяет нам работать с данными максимально эффективно. Индексация временной таблицы ИНДЕКСИРОВАТЬ ПО критически важна для быстрого выполнения ЛЕВОЕ СОЕДИНЕНИЕ, особенно когда строк больше нескольких сотен.
После получения общего результата запроса нам нужно распределить данные обратно по строкам документа. Выполнять поиск методом НайтиСледующий() в выборке для каждой строки — тоже не самый быстрый вариант. Оптимальным способом является использование объекта Соответствие (Map), где ключом будет ссылка на номенклатуру, а значением — цена. Проанализируем, как это реализовать:
ЦеныСоответствие = Новый Соответствие;
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
ЦеныСоответствие.Вставить(Выборка.Номенклатура, Выборка.Цена);
КонецЦикла;
// Теперь быстро заполняем документ
Для Каждого СтрокаТовара Из Объект.Товары Цикл
НайденнаяЦена = ЦеныСоответствие.Получить(СтрокаТовара.Номенклатура);
Если НайденнаяЦена <> Неопределено Тогда
СтрокаТовара.Цена = НайденнаяЦена;
КонецЕсли;
КонецЦикла;
Метод Получить() у объекта Соответствие работает практически мгновенно, так как использует хеш-таблицы. Это финальный штрих нашей оптимизации, который делает алгоритм линейным по сложности O(n) вместо квадратичной сложности O(n*m), которая была в самом начале.
Рассмотрим еще несколько важных правил, которые помогут избежать проблем с производительностью. Для анализа того, как эти запросы реально выполняются в системе, полезно использовать консоль запросов для управляемых форм с поддержкой логов технологического журнала:
Номенклатура.Артикул в условии запроса приводят к неявному соединению с таблицей справочника, что может быть избыточным. Старайтесь соединять таблицы явно.ВЫБРАТЬ только те поля, которые действительно будут использованы. Это снижает нагрузку на сеть и память сервера приложений.Запрос, разделяя тексты символом ";". Это позволяет выполнить несколько операций за одно обращение к серверу СУБД.ГДЕ. Это позволяет платформе 1С передать фильтры напрямую в SQL-запрос к СУБД, отсекая лишние данные на самом раннем этапе.Проанализировав вышеописанные методы, мы видим, что оптимизация кода 1С требует понимания того, как данные передаются между уровнями системы. Отказ от запросов в циклах в пользу массивов и временных таблиц — это базовый стандарт качественной разработки, который обеспечивает стабильную работу системы даже при больших объемах данных. Помните, что "красивый" и компактный код не всегда является производительным, поэтому всегда отдавайте предпочтение архитектуре, минимизирующей количество серверных вызовов.