← Назад к вопросам

Почему нельзя использовать запросы в цикле?

2.2 Middle🔥 191 комментариев
#Запросы и оптимизация#СУБД и хранение

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему нельзя использовать запросы в цикле

Краткий ответ

Можно, но это плохая практика, потому что:

  1. Катастрофическое падение производительности (N+1 проблема)
  2. Перегрузка СУБД множеством одиночных запросов
  3. Блокировки и deadlock-и в конкурентной среде
  4. Увеличение нагрузки на сеть

Проблема 1: N+1 запросов вместо 1

Представьте типичный сценарий:

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

// Если список 1000 товаров — это 1000 запросов к БД!

Временная сложность:

  • Один объединённый запрос: O(N)
  • N запросов в цикле: O(N²) или даже O(N × log N) с блокировками

Проблема 2: Нагрузка на сервер БД

Сервер БД для каждого запроса должен:

1. Получить запрос по сети
2. Распарсить SQL
3. Построить план выполнения (EXPLAIN)
4. Выполнить
5. Вернуть результат по сети
6. Закрыть курсор

Если 1000 товаров:

  • 1 правильный запрос: 6 шагов × 1 = 6 операций
  • 1000 запросов в цикле: 6 × 1000 = 6000 операций

Это 1000-кратный рост нагрузки на CPU, память и диск СУБД.

Проблема 3: Блокировки (Locks) и Deadlocks

// При циклическом выполнении запросов:

Структура блокировок:
  Запрос 1:  SELECT FROM Товары ГДЕ ID=1  // берёт shared lock
  Запрос 2:  SELECT FROM Товары ГДЕ ID=2  // берёт shared lock
  Запрос 3:  SELECT FROM Товары ГДЕ ID=3  // берёт shared lock
  ...
  Запрос N:  SELECT FROM Товары ГДЕ ID=N  // берёт shared lock
  
// Если одновременно идёт UPDATE где-то в БД
// Возможна блокировка (Lock Escalation)
// и даже DEADLOCK в многопроцессной среде

Проблема 4: Использование памяти

Каждый Запрос в 1С создаёт:

  • Объект-парсер SQL
  • Кэш плана выполнения
  • Буфер результатов
// ❌ ПЛОХО:
Для Каждого i = 1 По 10000 Цикл
    Запрос = Новый Запрос(...);  // 10000 объектов в памяти!
    Результат = Запрос.Выполнить();
КонецЦикла;
// Память: ~10000 × 100 КБ = 1 ГБ просто на объекты запросов!

Проблема 5: Задержка (Latency)

Если БД находится на другом сервере (что обычно):

Сценарий 1: Один запрос
  Отправка → 10мс
  Выполнение → 100мс
  Получение → 10мс
  Итого: 120мс

Сценарий 2: 1000 запросов
  (10мс + 100мс + 10мс) × 1000 = 120 000 мс = 120 секунд!
  (И это без сетевой конкуренции)

✅ ПРАВИЛЬНОЕ решение: один запрос с JOIN

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

Запрос.УстановитьПараметр("ТоварыДляОбработки", СписокТоваров);
Результат = Запрос.Выполнить();

Для Каждого Строка Из Результат.Выбрать() Цикл
    // Обработка строк
КонецЦикла;

// Один запрос = одно подключение = линейная сложность O(N)

Исключение 1: Когда можно использовать запрос в цикле

Если цикл маленький (< 10 итераций):

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

Если нельзя объединить (сложная бизнес-логика):

// Пример: для каждого товара вычислить цену с уникальным алгоритмом
Для Каждого Товар Из Товары Цикл
    ЦенаВычисленная = ВычислитьЦену(Товар);  // функция, не запрос
    // Запрос нужен только если ВычислитьЦену требует данных из БД
КонецЦикла;

Исключение 2: Когда необходимо изменять результат в цикле

// ❌ Неправильно пытаться избежать цикла:
Запрос = Новый Запрос("ВЫБРАТЬ ... ");
Результат = Запрос.Выполнить();
// Теперь нужно обновить каждую строку в БД
Для Каждого Строка Из Результат.Выбрать() Цикл
    УдалитьИзБД(Строка);  // эта операция требует ИНДИВИДУАЛЬНОЙ работы
КонецЦикла;

// ✅ Правильно: использовать UPDATE
Запрос = Новый Запрос(
    "УДАЛИТЬ ИЗ Таблица
     ГДЕ Условие = ИСТИНА"
);
Запрос.Выполнить();
// Один запрос на все удаления

Инструменты отладки для выявления проблемы

// Замер времени
Начало = ТекущаяДатаВМиллисекундах();
Для i = 1 По 1000 Цикл
    Запрос = Новый Запрос(...);
    Запрос.Выполнить();
КонецЦикла;
Конец = ТекущаяДатаВМиллисекундах();
Месообщение("Время: " + (Конец - Начало) + "мс");

// Результат:
// Циклический запрос: ~30000мс (30 сек)
// Один объединённый: ~500мс
// Разница: в 60 раз!

Итог

Никогда не используйте запросы в цикле за исключением случаев:

  1. Цикл очень маленький (< 10 итераций)
  2. Нельзя технически объединить запрос
  3. Нужна сложная логика между итерациями

Стратегия оптимизации:

  1. Один большой запрос с JOIN/IN clauses
  2. Если невозможно — используй временные таблицы
  3. Если совсем невозможно — параллелизуй запросы
  4. Последний вариант — кэширование результатов

Это одно из первых правил оптимизации в 1С и в любой системе с БД.

Почему нельзя использовать запросы в цикле? | PrepBro