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

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

Выполнение запросов внутри цикла при обходе строк табличной части или коллекций данных является одним из самых критичных антипаттернов в разработке на платформе 1С:Предприятие. Каждый вызов метода Выполнить() генерирует отдельное сетевое обращение к СУБД, что приводит к многократному увеличению нагрузки на сервер, возникновению избыточных блокировок и резкому падению производительности при росте количества строк.

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

Анализ проблемы и архитектурных ошибок

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


&НаСервере
Процедура ЗаполнитьНаСервере(НовыйОбъект)
    Товары = НовыйОбъект.Товары;
    Для Каждого Строка из Товары Цикл
        ПлановаяЦена = ПолучитьЗначениеЦеныПродажи(Строка.Номенклатура, НовыйОбъект.Ссылка.Дата, НовыйОбъект.Ссылка.Организация);        
        Строка.Себестоимость = Строка.Количество * ПлановаяЦена;
    КонецЦикла;
КонецПроцедуры

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

  1. Множественные запросы к СУБД: Если в документе содержится 100 строк, система выполнит 100 отдельных обращений к базе данных вместо одного пакетного запроса.
  2. Неявные соединения через точку: Обращение к реквизитам шапки через табличную часть документа (ОтчетПроизводстваЗаСменуПродукция.Ссылка.Организация) порождает на уровне SQL неявное левое соединение (LEFT JOIN), что создает лишнюю нагрузку на сервер.
  3. Отсутствие контроля статуса документа: Запрос к табличной части документов без проверки реквизитов Проведен = ИСТИНА и ПометкаУдаления = ЛОЖЬ может вернуть неактуальные данные из непроведенных или удаленных документов.
  4. Хранение цен в документах вместо регистров: Получение цен напрямую из документов не масштабируется. В типовых конфигурациях для хранения срезов цен и плановой себестоимости используются периодические регистры сведений (например, РегистрСведений.ЦеныНоменклатуры).

Способ 1. Получение данных одним запросом через массив номенклатуры и сопоставление через Соответствие

Рассмотрим первый классический вариант решения: предварительно соберем список всей номенклатуры из табличной части в массив, передадим его в единый запрос с условием ГДЕ Номенклатура В (&МассивНоменклатуры), а результат выгрузим в структуру быстрого поиска.

Для быстрого поиска цены в оперативной памяти эффективнее всего использовать объект Соответствие, так как обращение по ключу выполняется со сложностью O(1), в отличие от поиска по таблице значений методом НайтиСтроки(), требующего создания индексов.


&НаСервере
Процедура ЗаполнитьСебестоимостьЧерезМассив(НовыйОбъект)
    
    Товары = НовыйОбъект.Товары;
    
    // Получаем массив ссылок на номенклатуру без дублей
    МассивНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
    
    // Если табличная часть пуста, дальнейшие действия не требуются
    Если МассивНоменклатуры.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |	Цены.Номенклатура КАК Номенклатура,
    |	Цены.ПлановаяСтоимость КАК Цена
    |ИЗ
    |	Документ.ОтчетПроизводстваЗаСмену.Продукция КАК Цены
    |ГДЕ
    |	Цены.Ссылка.Организация = &Организация
    |	И Цены.Ссылка.Проведен = ИСТИНА
    |	И Цены.Ссылка.ПометкаУдаления = ЛОЖЬ
    |	И Цены.Номенклатура В(&МассивНоменклатуры)
    |	И Цены.Ссылка.Дата МЕЖДУ &ДатаНач И &ДатаКон";
    
    Запрос.УстановитьПараметр("Организация", НовыйОбъект.Организация);
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    Запрос.УстановитьПараметр("ДатаНач", НачалоМесяца(НовыйОбъект.Дата));
    Запрос.УстановитьПараметр("ДатаКон", КонецМесяца(НовыйОбъект.Дата));
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    // Заполняем соответствие для быстрого доступа к ценам
    СоответствиеЦен = Новый Соответствие;
    Пока Выборка.Следующий() Цикл
        СоответствиеЦен.Вставить(Выборка.Номенклатура, Выборка.Цена);
    КонецЦикла;
    
    // Заполняем табличную часть без выполнения запросов в цикле
    Для Каждого СтрокаТЧ Из Товары Цикл
        НайденнаяЦена = СоответствиеЦен[СтрокаТЧ.Номенклатура];
        ПлановаяЦена = ?(НайденнаяЦена = Неопределено, 0, НайденнаяЦена);
        
        СтрокаТЧ.Себестоимость = СтрокаТЧ.Количество * ПлановаяЦена;
    КонецЦикла;
    
КонецПроцедуры

Способ 2. Передача табличной части во временную таблицу запроса

Разберем второй вариант, когда в запрос передается вся выгруженная табличная часть объекта. Этот подход позволяет связать строки документа с регистром сведений или таблицей документов с помощью соединения (ЛЕВОЕ СОЕДИНЕНИЕ) и сразу получить готовую результирующую таблицу для обратной загрузки.

Важный нюанс: при передаче таблицы значений в запрос рекомендуется индексировать временную таблицу по полям соединения с помощью директивы ИНДЕКСИРОВАТЬ ПО.

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


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

Рекомендации по повышению производительности

  1. Индексация при поиске: Если сопоставление выполняется в ТаблицаЗначений через метод НайтиСтроки(), обязательно создавайте индекс по колонке поиска командой ТаблицаЗначений.Индексы.Добавить("Номенклатура"). Без индекса метод выполняет медленный полный перебор всех строк.
  2. Удаление дублей: При формировании параметров запроса передавайте массив только с уникальными ссылками (например, предварительно свернув таблицу значений перед получением колонки), чтобы не раздувать размер передаваемых в СУБД параметров.
  3. Использование виртуальных таблиц: Для получения срезов цен всегда отдавайте предпочтение виртуальным таблицам регистров сведений СрезПоследних с наложением условий в параметрах виртуальной таблицы, а не ручным выборкам из документов.
← На главную