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