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