Запрос в цикле: как найти и починить

«Обработка проведения тормозит на больших документах. Вот код — что не так?» — и в коде цикл по табличной части, а внутри Новый Запрос.

Запрос в цикле — антипаттерн, когда обращение к базе (запрос или чтение объекта) выполняется на каждой итерации цикла. Вместо одного похода в базу за всеми данными получается N походов — по одному на строку.

Это, наверное, самый частый вопрос собеседования про производительность в 1С. И самый показательный: умение увидеть запрос в цикле и переписать его на один запрос отличает джуна, который «пишет работающий код», от разработчика, чей код выдержит боевую базу.

Вопрос-крючок

Показывают примерно такой код — посчитать по каждой строке заказа остаток номенклатуры на складе:

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

Спрашивают: «В чём здесь проблема и как исправить?»

Почему это убивает производительность

Если в заказе 3 строки — вы почти ничего не заметите. Но код попадёт в документ на 500 строк, и это уже 500 отдельных запросов к базе. Каждый запрос — это не только сам расчёт остатка: это установка соединения с СУБД, парсинг текста запроса, построение плана, передача результата обратно. Накладные расходы на один поход в базу умножаются на число итераций.

Хуже того, если этот цикл сам вложен во внешний цикл (например, по документам за день), число запросов перемножается: 200 документов по 500 строк — это 100 000 запросов там, где хватило бы одного-двух. База начинает захлёбываться, растёт время отклика, копятся блокировки, другие пользователи ждут. Классический сценарий «под конец месяца всё встало».

Как чинить: правило «сначала собрать, потом обработать»

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

Способ 1: один запрос с соединением табличной части

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

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

Здесь Заказ.Товары.Выгрузить() превращает табличную часть в таблицу значений, которую запрос принимает через параметр и соединяет с остатками. ЛЕВОЕ СОЕДИНЕНИЕ гарантирует, что строки без остатков не пропадут, а ЕСТЬNULL(..., 0) подставит ноль там, где остатка нет (про NULL — следующий урок).

Способ 2: временная таблица (когда данных много)

Если строк очень много или их надо соединять несколько раз, аккуратнее положить их во временную таблицу через менеджер временных таблиц — платформа проиндексирует её и оптимизатор СУБД построит план лучше:

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

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

Обратите внимание: отбор остатков сужен параметром Номенклатура В (...) — мы считаем остатки сразу только по номенклатуре из заказа, а не по всему справочнику. Это ровно тот приём из прошлого урока: фильтруем в параметрах виртуальной таблицы.

Как искать такие места

На собеседовании ценят, если вы знаете не только «как переписать», но и «как найти»:

  • Поиск по коду. Ищем Новый Запрос и глазами проверяем, не стоит ли оно внутри Для ... Цикл или Пока ... Цикл. То же с .Выгрузить(), .НайтиПоКоду(), ПолучитьОбъект() внутри циклов.
  • Замер производительности. В конфигураторе есть замер производительности (профилировщик): он покажет строки, где суммарно «висит» время. Запрос в цикле обычно и есть самая горячая строка.
  • Технологический журнал / АПДЕКС. На боевой базе всплеск числа однотипных коротких запросов — верный признак цикла.
  • Здравый смысл. Любое обращение к базе (Запрос, чтение объекта, .Записать()) внутри цикла по данным — повод насторожиться.

Частые ошибки на собеседовании

  • «На тесте же работает быстро». На трёх строках работает всё. Проблема вылезает на боевых объёмах — именно это и проверяет вопрос.
  • Вынести Новый Запрос за цикл, но .Выполнить() оставить внутри. Создание объекта запроса дёшево; дорого — само обращение к базе. Из цикла надо убирать Выполнить(), а не конструктор.
  • Забыть ЛЕВОЕ СОЕДИНЕНИЕ и потерять строки. При ВНУТРЕННЕМ соединении строки заказа без остатков просто исчезнут из результата — данные «пропадут».
  • Не свернуть повторы. Если в заказе одна и та же номенклатура в двух строках, при соединении полезно помнить про дубли; иногда нужен СГРУППИРОВАТЬ ПО или РАЗЛИЧНЫЕ.

Итоги-шпаргалка

  • Запрос в цикле = N обращений к базе вместо одного; на боевых объёмах это главный убийца производительности.
  • Правило: сначала собрать все данные одним запросом, потом обрабатывать результат в цикле.
  • Табличную часть отдаём в запрос через .Выгрузить() и соединяем с виртуальной таблицей.
  • Много данных или несколько соединений — используем временную таблицу (ПОМЕСТИТЬ + ИНДЕКСИРОВАТЬ ПО).
  • Ищем через поиск Новый Запрос в циклах и через замер производительности в конфигураторе.
Проверьте себя
1. Почему антипаттерн «запрос в цикле» так вредит производительности на больших документах?
AКаждая итерация делает отдельное обращение к базе, и накладные расходы умножаются на число строк
BЦиклы в 1С в принципе работают медленно, дело не в запросе
CВнутри цикла нельзя использовать параметры запроса
DПроблема только в оперативной памяти, к базе это не относится
2. Как правильно переписать код, который в цикле по табличной части читает остаток каждой номенклатуры?
AВынести только строку Новый Запрос за цикл, а Выполнить() оставить внутри
BОдним запросом соединить выгруженную табличную часть с виртуальной таблицей остатков и обработать готовый результат
CЗаменить цикл Для Каждого на цикл Пока — так быстрее
DДобавить внутрь цикла установку блокировки, чтобы данные не менялись