Как ускорить проведение документов и правильно убрать запросы из цикла в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Одна из самых распространенных причин низкой производительности в 1С — это выполнение запросов внутри цикла. Часто начинающие разработчики при проведении документа перебирают строки табличной части и для каждой строки выполняют отдельный запрос к базе данных (например, для проверки остатков или получения цены). Это приводит к колоссальной нагрузке на сервер СУБД и значительному увеличению времени транзакции.

В этой статье мы подробно разберем, почему так делать нельзя, и рассмотрим пошаговое решение проблемы на конкретных примерах кода. Мы научимся оптимизировать алгоритмы проведения, используя пакетную обработку данных.

Почему запрос в цикле — это плохо?

Давайте проанализируем, что происходит, когда вы пишете конструкцию Запрос.Выполнить() внутри цикла Для Каждого....

Представим, что в вашем документе 100 строк. Если вы делаете запрос в цикле, платформа 1С вынуждена:

  1. 100 раз обратиться к серверу приложений.
  2. 100 раз скомпилировать текст запроса (даже если он одинаковый).
  3. 100 раз отправить команду SQL-серверу.
  4. 100 раз получить ответ и передать его обратно.

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

Золотое правило оптимизации 1С: Всегда получайте все необходимые данные одним запросом до начала цикла обработки.

Этап 1: Подготовка данных для запроса

Вместо того чтобы спрашивать базу данных о каждой номенклатуре отдельно, мы соберем все интересующие нас товары в один список или массив. Рассмотрим пример документа "РеализацияТоваровУслуг", где нам нужно проверить остатки по каждой позиции.

Сначала выгрузим данные из табличной части документа. Нас интересует колонка "Номенклатура".


// Получаем массив номенклатуры из табличной части документа
// Используем метод ВыгрузитьКолонку, чтобы не бегать циклом по ТЧ
МассивНоменклатуры = ЭтотОбъект.Товары.ВыгрузитьКолонку("Номенклатура");

// Если в документе могут быть дубли строк, имеет смысл свернуть массив
// Для этого можно выгрузить в ТаблицуЗначений и использовать метод Свернуть
ТаблицаТоваров = ЭтотОбъект.Товары.Выгрузить();
ТаблицаТоваров.Свернуть("Номенклатура, Склад", "Количество");

Использование метода ВыгрузитьКолонку работает очень быстро и позволяет нам сразу получить массив ссылок, который мы передадим в запрос.

Этап 2: Написание пакетного запроса

Теперь создадим запрос, который получит остатки сразу по всему списку номенклатуры. Для этого мы будем использовать оператор В (IN) в условии запроса.

Разберем структуру правильного запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
    |	ТоварыНаСкладахОстатки.КоличествоОстаток КАК Остаток
    |ИЗ
    |	РегистрНакопления.ТоварыНаСкладах.Остатки(
    |			&МоментВремени,
    |			Номенклатура В (&СписокНоменклатуры)
    |				И Склад = &Склад) КАК ТоварыНаСкладахОстатки";

// Устанавливаем параметры
Запрос.УстановитьПараметр("МоментВремени", ЭтотОбъект.МоментВремени());
Запрос.УстановитьПараметр("Склад", ЭтотОбъект.Склад);
// Передаем массив, который мы подготовили на этапе 1
Запрос.УстановитьПараметр("СписокНоменклатуры", МассивНоменклатуры);

Обратите внимание на использование параметра &СписокНоменклатуры. Платформа 1С автоматически преобразует массив ссылок в SQL-конструкцию вида WHERE Nomenclature IN (...). Это позволяет СУБД за один проход индекса найти все нужные записи.

Этап 3: Обработка результатов запроса

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

Рассмотрим вариант с выгрузкой в таблицу значений и поиском через НайтиСтроки или индексированием. Для максимальной скорости на больших объемах рекомендуется использовать соответствие (Map) или индексированную таблицу.

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


// Выполняем запрос
РезультатЗапроса = Запрос.Выполнить();
ТаблицаОстатков = РезультатЗапроса.Выгрузить();

// Обязательно индексируем колонку поиска для ускорения метода Найти
ТаблицаОстатков.Индексы.Добавить("Номенклатура");

// Теперь идем циклом по строкам документа
Для Каждого СтрокаДокумента Из ЭтотОбъект.Товары Цикл
    
    // Ищем остаток в полученной таблице. Это происходит в памяти, без обращения к БД
    Отбор = Новый Структура("Номенклатура", СтрокаДокумента.Номенклатура);
    НайденныеСтроки = ТаблицаОстатков.НайтиСтроки(Отбор);
    
    ДоступныйОстаток = 0;
    Если НайденныеСтроки.Количество() > 0 Тогда
        ДоступныйОстаток = НайденныеСтроки[0].Остаток;
    КонецЕсли;
    
    // Проверяем условие
    Если СтрокаДокумента.Количество > ДоступныйОстаток Тогда
        Сообщение = Новый СообщениеПользователю;
        Сообщение.Текст = "Не хватает товара: " + СтрокаДокумента.Номенклатура;
        Сообщение.Сообщить();
        Отказ = Истина;
    КонецЕсли;
    
КонецЦикла;

В данном примере мы используем метод НайтиСтроки объекта ТаблицаЗначений. Благодаря предварительному созданию индекса через Индексы.Добавить, поиск происходит практически мгновенно даже на тысячах строк.

Продвинутый метод: Использование Менеджера временных таблиц

Если логика проведения сложная и требует соединения данных документа с несколькими регистрами (например, остатки, цены, резервы), то простого оператора В может быть недостаточно или он станет громоздким. В таких случаях мы используем МенеджерВременныхТаблиц.

Посмотрим, как это реализовать:

  1. Помещаем табличную часть документа во временную таблицу в запросе.
  2. Соединяем эту временную таблицу с таблицами регистров.
  3. Получаем итоговый результат одним пакетом.

Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;

// Шаг 1: Помещаем данные документа во временную таблицу
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ТЧ.Номенклатура КАК Номенклатура,
    |	ТЧ.Количество КАК КоличествоВДокументе,
    |	ТЧ.НомерСтроки КАК НомерСтроки
    |ПОМЕСТИТЬ ВТ_Документ
    |ИЗ
    |	&ТЧ КАК ТЧ";

Запрос.УстановитьПараметр("ТЧ", ЭтотОбъект.Товары.Выгрузить());
Запрос.Выполнить();

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

Запрос.УстановитьПараметр("Момент", МоментВремени());

Выборка = Запрос.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл
    Если Выборка.КоличествоВДокументе > Выборка.Остаток Тогда
        // Обработка нехватки
    КонецЕсли;
КонецЦикла;

Этот метод является наиболее профессиональным и масштабируемым. Он позволяет обрабатывать сложные условия контроля, используя всю мощь языка запросов, вместо того чтобы писать многоступенчатые условия Если внутри цикла.

Важные нюансы при оптимизации

Рассмотрим несколько деталей, о которых не стоит забывать:

Используя описанные выше подходы, вы сможете ускорить проведение документов в десятки раз, особенно в многопользовательском режиме работы.

← На главную