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

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

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

Анализ типичных ошибок исходного кода

Рассмотрим фрагмент кода, с которого началась проблема:


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

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

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

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

Если требуется получить данные из одного последнего созданного документа инвентаризации, разберем по шагам, как вынести запрос за пределы цикла и выполнить сопоставление через метод Найти():

  1. Выполняем один запрос к табличной части последней инвентаризации (ограничивая выборку подзапросом с сортировкой по убыванию даты).
  2. Выгружаем результат запроса в локальную таблицу значений ТЗ_Инвентаризация.
  3. Обходим строки целевой таблицы и ищем соответствие по номенклатуре.

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


Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    ИнвТовары.Номенклатура КАК Номенклатура,
|    ИнвТовары.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
|    ИнвТовары.Количество КАК Количество,
|    ИнвТовары.Цена КАК Цена
|ИЗ
|    Документ.ИнвентаризацияТоваровНаСкладе.Товары КАК ИнвТовары
|ГДЕ
|    ИнвТовары.Ссылка В
|        (ВЫБРАТЬ ПЕРВЫЕ 1
|            ИнвШапка.Ссылка
|        ИЗ
|            Документ.ИнвентаризацияТоваровНаСкладе КАК ИнвШапка
|        ГДЕ
|            ИнвШапка.Проведен = ИСТИНА
|            И НЕ ИнвШапка.ПометкаУдаления
|            И ИнвШапка.Дата <= &ДатаДокумента
|        УПОРЯДОЧИТЬ ПО
|            ИнвШапка.Дата УБЫВ)";

Запрос.УстановитьПараметр("ДатаДокумента", Объект.Дата);

ТЗ_Инвентаризация = Запрос.Выполнить().Выгрузить();

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

Решение 2: Оптимальный пакетный запрос с временной таблицей

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

Рассмотрим подробнее профессиональный способ решения с передачей исходной таблицы во временную таблицу (ВТ) и вычислением последней цены для каждой номенклатуры индивидуально:


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

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

РезультатЗапроса = Запрос.Выполнить().Выгрузить();

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

Архитектурные рекомендации

Выясним причину, почему хранение и получение цен непосредственно из документов инвентаризации является проблемным решением:

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