Запрос в цикле: как найти и починить
«Обработка проведения тормозит на больших документах. Вот код — что не так?» — и в коде цикл по табличной части, а внутри Новый Запрос.
Запрос в цикле — антипаттерн, когда обращение к базе (запрос или чтение объекта) выполняется на каждой итерации цикла. Вместо одного похода в базу за всеми данными получается N походов — по одному на строку.
Это, наверное, самый частый вопрос собеседования про производительность в 1С. И самый показательный: умение увидеть запрос в цикле и переписать его на один запрос отличает джуна, который «пишет работающий код», от разработчика, чей код выдержит боевую базу.
Вопрос-крючок
Показывают примерно такой код — посчитать по каждой строке заказа остаток номенклатуры на складе:
Для Каждого СтрокаТовара Из Заказ.Товары Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ОстаткиТоваров.КоличествоОстаток
|ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки(, Номенклатура = &Ном) КАК ОстаткиТоваров";
Запрос.УстановитьПараметр("Ном", СтрокаТовара.Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
СтрокаТовара.Остаток = Выборка.КоличествоОстаток;
КонецЕсли;
КонецЦикла;Спрашивают: «В чём здесь проблема и как исправить?»
Почему это убивает производительность
Если в заказе 3 строки — вы почти ничего не заметите. Но код попадёт в документ на 500 строк, и это уже 500 отдельных запросов к базе. Каждый запрос — это не только сам расчёт остатка: это установка соединения с СУБД, парсинг текста запроса, построение плана, передача результата обратно. Накладные расходы на один поход в базу умножаются на число итераций.
Хуже того, если этот цикл сам вложен во внешний цикл (например, по документам за день), число запросов перемножается: 200 документов по 500 строк — это 100 000 запросов там, где хватило бы одного-двух. База начинает захлёбываться, растёт время отклика, копятся блокировки, другие пользователи ждут. Классический сценарий «под конец месяца всё встало».
Как чинить: правило «сначала собрать, потом обработать»
Идея простая: не ходить в базу внутри цикла. Сначала одним запросом получить все нужные данные сразу, а потом в цикле только раскладывать готовый результат. Есть два основных способа.
Способ 1: один запрос с соединением табличной части
Табличную часть документа можно передать в запрос как источник данных и соединить её с виртуальной таблицей остатков. Тогда весь расчёт делается на стороне СУБД за один заход:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Номенклатура КАК Номенклатура,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток
|ИЗ
| &Товары КАК Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки() КАК Остатки
| ПО Остатки.Номенклатура = Товары.Номенклатура";
Запрос.УстановитьПараметр("Товары", Заказ.Товары.Выгрузить());
ВыборкаОстатков = Запрос.Выполнить().Выбрать();Здесь Заказ.Товары.Выгрузить() превращает табличную часть в таблицу значений, которую запрос принимает через параметр и соединяет с остатками. ЛЕВОЕ СОЕДИНЕНИЕ гарантирует, что строки без остатков не пропадут, а ЕСТЬNULL(..., 0) подставит ноль там, где остатка нет (про NULL — следующий урок).
Способ 2: временная таблица (когда данных много)
Если строк очень много или их надо соединять несколько раз, аккуратнее положить их во временную таблицу через менеджер временных таблиц — платформа проиндексирует её и оптимизатор СУБД построит план лучше:
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст =
"ВЫБРАТЬ Товары.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ &Товары КАК Товары
|ИНДЕКСИРОВАТЬ ПО Номенклатура";
Запрос.УстановитьПараметр("Товары", Заказ.Товары.Выгрузить());
Запрос.Выполнить();
Запрос.Текст =
"ВЫБРАТЬ
| ВТ_Товары.Номенклатура,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток
|ИЗ
| ВТ_Товары КАК ВТ_Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки(
| , Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_Товары)) КАК Остатки
| ПО Остатки.Номенклатура = ВТ_Товары.Номенклатура";
Выборка = Запрос.Выполнить().Выбрать();Обратите внимание: отбор остатков сужен параметром Номенклатура В (...) — мы считаем остатки сразу только по номенклатуре из заказа, а не по всему справочнику. Это ровно тот приём из прошлого урока: фильтруем в параметрах виртуальной таблицы.
Как искать такие места
На собеседовании ценят, если вы знаете не только «как переписать», но и «как найти»:
- Поиск по коду. Ищем
Новый Запроси глазами проверяем, не стоит ли оно внутриДля ... ЦиклилиПока ... Цикл. То же с.Выгрузить(),.НайтиПоКоду(),ПолучитьОбъект()внутри циклов. - Замер производительности. В конфигураторе есть замер производительности (профилировщик): он покажет строки, где суммарно «висит» время. Запрос в цикле обычно и есть самая горячая строка.
- Технологический журнал / АПДЕКС. На боевой базе всплеск числа однотипных коротких запросов — верный признак цикла.
- Здравый смысл. Любое обращение к базе (
Запрос, чтение объекта,.Записать()) внутри цикла по данным — повод насторожиться.
Частые ошибки на собеседовании
- «На тесте же работает быстро». На трёх строках работает всё. Проблема вылезает на боевых объёмах — именно это и проверяет вопрос.
- Вынести
Новый Запросза цикл, но.Выполнить()оставить внутри. Создание объекта запроса дёшево; дорого — само обращение к базе. Из цикла надо убиратьВыполнить(), а не конструктор. - Забыть
ЛЕВОЕ СОЕДИНЕНИЕи потерять строки. ПриВНУТРЕННЕМсоединении строки заказа без остатков просто исчезнут из результата — данные «пропадут». - Не свернуть повторы. Если в заказе одна и та же номенклатура в двух строках, при соединении полезно помнить про дубли; иногда нужен
СГРУППИРОВАТЬ ПОилиРАЗЛИЧНЫЕ.
Итоги-шпаргалка
- Запрос в цикле = N обращений к базе вместо одного; на боевых объёмах это главный убийца производительности.
- Правило: сначала собрать все данные одним запросом, потом обрабатывать результат в цикле.
- Табличную часть отдаём в запрос через
.Выгрузить()и соединяем с виртуальной таблицей. - Много данных или несколько соединений — используем временную таблицу (
ПОМЕСТИТЬ+ИНДЕКСИРОВАТЬ ПО). - Ищем через поиск
Новый Запросв циклах и через замер производительности в конфигураторе.