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

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

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

Проблема запросов в цикле и причин задержек

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

Выполнение запросов в цикле создает следующие проблемы:

  1. Множественные обращения к СУБД: Вместо одного обращения платформа генерирует сотни и тысячи SQL-запросов к серверу базы данных.
  2. Повторная компиляция планов запросов: СУБД вынуждена постоянно пересчитывать план выполнения при частой смене параметров.
  3. Избыточный перебор данных: Время выполнения алгоритма растет экспоненциально с увеличением количества строк в документе.

Решение 1: Вынос запроса из цикла с выгрузкой в таблицу значений

Разберем базовый способ оптимизации. Сначала мы собираем массив всей номенклатуры из табличной части документа, передаем его в параметр запроса и получаем обороты за весь период недели за один раз. Затем выгружаем результат в таблицу значений и в цикле ищем нужные строки.

Посмотрим на пример реализации данного подхода:


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

    Запрос.УстановитьПараметр("Подразделение", Документ.Подразделение);
    Запрос.УстановитьПараметр("ДатаНач", НачалоДня(Документ.НачНедели));
    Запрос.УстановитьПараметр("ДатаКон", КонецДня(НачалоДня(Документ.НачНедели) + 6 * 86400));
    Запрос.УстановитьПараметр("МассивНоменклатуры", Документ.Товары.ВыгрузитьКолонку("Номенклатура"));

    ТЗ = Запрос.Выполнить().Выгрузить();
    // Для ускорения поиска в памяти создаем индекс
    ТЗ.Индексы.Добавить("Номенклатура, Период");

    Отбор = Новый Структура("Номенклатура, Период");

    Для Каждого Стр Из Документ.Товары Цикл
        ТекДень = НачалоДня(Документ.НачНедели);
        ОбщееКоличество = 0;
        ОбщаяСумма = 0;

        Для сч = 1 По 7 Цикл
            Отбор.Номенклатура = Стр.Номенклатура;
            Отбор.Период = ТекДень;
            
            СтрокиТЗ = ТЗ.НайтиСтроки(Отбор);
            Если СтрокиТЗ.Количество() Тогда
                Выборка = СтрокиТЗ[0];
                
                Количество = Выборка.Количество;
                Сумма = Выборка.Сумма;
                
                Стр["ДеньНедели" + сч] = Количество;
                Стр["ЦенаДень" + сч] = Сумма;
                
                ОбщееКоличество = ОбщееКоличество + Количество;
                ОбщаяСумма = ОбщаяСумма + Сумма;
            КонецЕсли;

            ТекДень = ТекДень + 86400;
        КонецЦикла;

        Если ОбщаяСумма <> 0 И ОбщееКоличество <> 0 Тогда
            Стр.СредняяЦена = ОбщаяСумма / ОбщееКоличество;
        КонецЕсли;

        Стр.ОстатокПоСистеме = Стр.Отправлено - ОбщееКоличество;
        Стр.Продано = ОбщееКоличество;
        
    КонецЦикла;

    Документ.Товары.Сортировать("Номенклатура");
КонецПроцедуры

Для ускорения поиска в полученной таблице значений крайне важно использовать индексацию. Вызов метода ТЗ.Индексы.Добавить("Номенклатура, Период") переводит поиск с помощью НайтиСтроки() из линейного $O(N)$ в быстрый бинарный поиск.

Решение 2: Перенос разворота по дням прямо в текст запроса (Пивот)

Еще более эффективный подход — переложить всю работу по группировке и распределению продаж по дням недели на сервер СУБД. С помощью конструкции ВЫБОР КОГДА мы можем развернуть дни недели в отдельные колонки непосредственно в запросе.

Проанализируем пример такого запроса:


ВЫБРАТЬ
	ПродажиОбороты.Номенклатура КАК Номенклатура,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 2 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели1,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 3 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели2,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 4 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели3,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 5 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели4,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 6 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели5,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 7 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели6,
	СУММА(ВЫБОР КОГДА ДЕНЬНЕДЕЛИ(ПродажиОбороты.Период) = 1 ТОГДА ПродажиОбороты.КоличествоОборот ИНАЧЕ 0 КОНЕЦ) КАК ДеньНедели7,
	СУММА(ПродажиОбороты.КоличествоОборот) КАК ОбщееКоличество,
	СУММА(ПродажиОбороты.СтоимостьОборот) КАК ОбщаяСумма
ИЗ
	РегистрНакопления.Продажи.Обороты(
		&ДатаНач,
		&ДатаКон,
		День,
		Номенклатура В (&МассивНоменклатуры)
			И Подразделение = &Подразделение) КАК ПродажиОбороты
СГРУППИРОВАТЬ ПО
	ПродажиОбороты.Номенклатура

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

Рекомендации и лучшие практики

  1. Использование временных таблиц: При передаче больших массивов номенклатуры предпочтительнее загружать их во временную таблицу с помощью МенеджерВременныхТаблиц и индексировать поле Номенклатура.
  2. Избегайте неявных запросов: При обращении в цикле к реквизитам через точку (например, Стр.Номенклатура.Артикул) генерируются скрытые запросы к базе данных. Выбирайте все необходимые реквизиты сразу в основном запросе.
  3. Заполняйте свойства автоматически: Использование метода ЗаполнитьЗначенияСвойств(СтрТабличнойЧасти, Выборка) ускоряет работу интерпретатора 1С и избавляет от громоздких условий Если ... ИначеЕсли.
← На главную