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

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

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

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

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


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

Пока Выборка.Следующий Цикл
    // Пройти по строкам табличной части
    Для Каждого Строка Из Объект.Товары Цикл
        // Найти строку с соответствующей номенклатурой
        Если Строка.Номенклатура = Выборка.Номенклатура Тогда
            // Установить цену для найденной строки
            Строка.Цена = Выборка.Цена;
            //Прервать;
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

Основные недостатки этого подхода:

  1. Неэффективное соединение в запросе: Использование ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваровИУслуг.Товары напрямую в запросе к регистру цен не имеет смысла, если документ ещё не записан или если мы хотим получить цены для номенклатуры, которая уже есть в табличной части, а не для всех возможных товаров. Если документ уже записан, этот запрос будет брать номенклатуру из записанных строк. Если же документ новый, то эта таблица будет пуста, и запрос не вернет нужных данных. Более того, запрос фактически выполняет соединение всех возможных цен со всеми товарами из существующего документа (если он уже сохранен), а не только с теми, что есть в текущей редактируемой табличной части.
  2. Вложенные циклы: Самый критичный момент. Мы имеем цикл по выборке цен (Пока Выборка.Следующий Цикл) и внутри него — цикл по строкам табличной части документа (Для Каждого Строка Из Объект.Товары Цикл). Это приводит к квадратичной сложности алгоритма (O(N*M), где N – количество цен, M – количество строк в табличной части). При большом количестве строк такая операция будет выполняться очень долго.
  3. Многократный поиск: В каждом внутреннем цикле происходит линейный поиск по номенклатуре (Если Строка.Номенклатура = Выборка.Номенклатура Тогда).

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

Решение 1: Оптимизация поиска в цикле с использованием метода "Найти"

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

Метод Найти() ищет первую строку, удовлетворяющую заданному условию, а НайтиСтроки() возвращает массив всех строк. Поскольку номенклатура в табличной части, как правило, уникальна или нам нужна первая подходящая строка, Найти() будет более подходящим.

Как работает метод Найти()

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

Рассмотрим пример кода с применением Найти():


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

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

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

Пока Выборка.Следующий() Цикл
    // Ищем строку в табличной части по найденной номенклатуре
    СтрокаТЧ = Объект.Товары.Найти(Выборка.Номенклатура, "Номенклатура");
    Если СтрокаТЧ <> Неопределено Тогда
        // Устанавливаем цену
        СтрокаТЧ.Цена = Выборка.Цена;
    КонецЕсли;
КонецЦикла;

Преимущества этого подхода:

  1. Оптимизированный поиск: Метод Найти() выполняется значительно быстрее, чем ручной перебор, особенно для больших табличных частей.
  2. Уменьшение выборки: Мы добавили условие ГДЕ ЦеныНоменклатурыСрезПоследних.Номенклатура В (&СписокНоменклатуры), что позволяет получить цены только для той номенклатуры, которая фактически присутствует в нашей табличной части, а не для всей номенклатуры регистра цен.

Недостатки:

  1. Хотя это улучшение, мы по-прежнему обходим табличную часть дважды: один раз для сбора списка номенклатуры и один раз косвенно через Найти() в цикле по выборке. Для очень больших табличных частей это все еще может быть не самым оптимальным решением.

Решение 2: Передача данных табличной части во временную таблицу запроса (наиболее оптимальный вариант)

Наиболее производительным и правильным подходом для таких задач является использование механизма временных таблиц в запросах. Этот подход позволяет передать данные из табличной части документа на сервер, обработать их там с помощью запроса (соединив с регистром цен) и получить уже готовые данные, содержащие цены для каждой номенклатуры.

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

Шаги реализации:

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

Разберем пример кода для данного подхода:


// 1. Создаем менеджер временных таблиц
МВТ = Новый МенеджерВременныхТаблиц;

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

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

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

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

ЗапросОсновной.УстановитьПараметр("ВидЦены", Объект.ВидЦены);

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

// 4. Обход результата запроса и заполнение табличной части документа
Пока ВыборкаЦен.Следующий() Цикл
    СтрокаТЧ = Объект.Товары.Найти(ВыборкаЦен.Номенклатура, "Номенклатура");
    Если СтрокаТЧ <> Неопределено Тогда
        СтрокаТЧ.Цена = ВыборкаЦен.Цена;
    КонецЕсли;
КонецЦикла;

Преимущества этого подхода:

  1. Максимальная производительность: Все основные операции по поиску и сопоставлению цен выполняются на сервере СУБД, что значительно быстрее, чем обработка в циклах на стороне 1С.
  2. Оптимизация сетевого трафика: На клиент передается только конечный результат – список номенклатуры с уже найденными ценами.
  3. Гибкость: Временные таблицы позволяют строить сложные запросы, объединяя данные из разных источников.

Дополнительный вариант с передачей ссылки на строку и индексом

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

Однако, передавать саму строку табличной части как параметр в запрос напрямую невозможно. Вместо этого мы можем передать уникальный идентификатор строки или, что чаще, номер строки вместе с номенклатурой. Тогда запрос вернет Номенклатуру, НомерСтроки и Цену. После этого мы можем обратиться к строке табличной части по номеру (Объект.Товары[НомерСтроки - 1]).

Рассмотрим пример такого подхода:


// 1. Создаем менеджер временных таблиц
МВТ = Новый МенеджерВременныхТаблиц;

// 2. Создаем ТаблицуЗначений с Номенклатурой и НомеромСтроки
ТаблицаНоменклатурыСНомерами = Новый ТаблицаЗначений;
ТаблицаНоменклатурыСНомерами.Колонки.Добавить("Номенклатура");
ТаблицаНоменклатурыСНомерами.Колонки.Добавить("НомерСтроки", Новый ОписаниеТипов("Число"));

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

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

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

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

ЗапросОсновной.УстановитьПараметр("ВидЦены", Объект.ВидЦены);

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

// 4. Обход результата запроса и заполнение табличной части
Пока ВыборкаЦен.Следующий() Цикл
    Если ВыборкаЦен.НомерСтроки < Объект.Товары.Количество() Тогда // Проверяем, что индекс актуален
        СтрокаТЧ = Объект.Товары[ВыборкаЦен.НомерСтроки]; // Прямое обращение по индексу
        СтрокаТЧ.Цена = ВыборкаЦен.Цена;
    КонецЕсли;
КонецЦикла;

Примечание: Если в табличной части могут быть повторяющиеся номенклатуры, и мы хотим заполнить цены для всех из них, то метод Найти() найдет только первую. В этом случае, если нам нужно обработать все строки, метод НайтиСтроки() вернет массив строк, который можно обойти. Однако, более правильный подход с временными таблицами, если в ВТ_НоменклатураДокумента мы будем передавать не только Номенклатуру, но и УникальныйИдентификаторСтрокиТЧ (если такой реквизит есть) или НомерСтроки. В примере выше с НомерСтроки мы гарантируем заполнение именно нужной строки. Если же номенклатура повторяется и цены для всех одинаковые, можно обойтись и Найти().

Решение 3: Проверка стандартных механизмов заполнения цен в конфигурации

Перед тем как писать собственный код, всегда важно проверить, не реализован ли уже подобный функционал в типовой конфигурации. Многие стандартные конфигурации 1С (Управление Торговлей, Комплексная Автоматизация, ERP) имеют развитые механизмы работы с ценами.

  1. Автоматическое заполнение при выборе контрагента или вида цен: Часто в документе есть поле "Вид цены", и при его выборе или при изменении контрагента система может автоматически пересчитывать цены в табличной части. Проверьте наличие соответствующих подписок на события или обработчиков в модуле формы документа.
  2. Кнопка "Заполнить" в документе: Посмотрите, нет ли на командной панели табличной части кнопки "Заполнить" или "Заполнить цены", которая уже выполняет требуемый функционал.
  3. Настройка видов цен: Убедитесь, что типы цен номенклатуры правильно настроены в справочнике "Виды цен номенклатуры" и регулярно устанавливаются с помощью документа "Установка цен номенклатуры". Правильная настройка является основой для любых механизмов ценообразования.

Использование типового функционала всегда предпочтительнее собственной разработки, так как это снижает затраты на поддержку и обновление.

Дополнительные рекомендации по оптимизации запросов и работы с данными

Для достижения максимальной производительности при работе с 1С и запросами, всегда обращайте внимание на следующие аспекты:

  1. Индексирование полей: Убедитесь, что поля, по которым происходит соединение или отбор в запросах (например, Номенклатура, ВидЦены в регистре цен), имеют индексы в базе данных. 1С обычно сама создает индексы для ключевых полей, но для нестандартных условий это может потребовать анализа.
  2. Избегайте "обращений через две точки" в условиях отбора: В запросах, особенно в фильтрах виртуальных таблиц (таких как СрезПоследних), избегайте условий типа ВидЦены = &ВидЦены, где &ВидЦены является ссылкой на объект, а не его идентификатором. Если ВидЦены в регистре имеет составной тип, а &ВидЦены — конкретный тип, может потребоваться явное приведение типа с помощью ВЫРАЗИТЬ, чтобы СУБД могла эффективно использовать индексы. Однако, для обычных ссылочных типов это обычно не проблема.
  3. Использование Менеджера Временных Таблиц (МВТ): Всегда используйте МенеджерВременныхТаблиц, когда нужно передать результаты одного запроса в другой или создать последовательность запросов, работающих с одними и теми же временными данными. Это намного эффективнее, чем создавать промежуточные ТаблицыЗначений в коде 1С и передавать их как параметры.
  4. Размер временных таблиц: Имейте в виду, что временные таблицы хранятся в памяти сервера СУБД или на диске. Очень большие временные таблицы могут потреблять значительные ресурсы. Оптимизируйте данные, которые вы помещаете во временные таблицы, оставляя только необходимое.
  5. Ограничивайте выборку: Всегда старайтесь ограничивать выборку данных только тем, что действительно необходимо. Используйте условия ГДЕ и ПЕРИОД в виртуальных таблицах для минимизации объема обрабатываемых данных.

Заключение

Мы рассмотрели различные подходы к заполнению табличной части документа ценами в 1С. Исходный код автора, хотя и выполняет свою задачу, является крайне неэффективным из-за вложенных циклов и неоптимизированного поиска. Мы предложили следующие улучшения:

  1. Использование метода Объект.Товары.Найти() вместо ручного перебора для оптимизации поиска внутри цикла. Этот подход является базовым улучшением.
  2. Наиболее оптимальное решение – использование временных таблиц в запросах. Этот метод позволяет выполнять все ресурсоемкие операции по поиску и сопоставлению данных на стороне СУБД, что обеспечивает максимальную производительность. Мы показали, как передать номенклатуру и номер строки табличной части во временную таблицу, а затем использовать ее для эффективного получения цен.
  3. Важно всегда проверять наличие типовых механизмов заполнения цен в конфигурации, чтобы не изобретать велосипед.

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

← На главную