При разработке функционала для документов в 1С, особенно связанных с заполнением табличных частей большим количеством позиций, крайне важно уделять внимание производительности. Часто возникает задача заполнения цен для номенклатуры в табличной части документа на основании выбранного вида цен. Давайте рассмотрим стандартные подходы, разберем распространённые ошибки и предложим оптимальные решения для этой задачи.
Исходный код, предложенный автором, демонстрирует желание заполнить цены в табличной части документа "ПоступлениеТоваровИУслуг" на основании регистра сведений "ЦеныНоменклатуры.СрезПоследних" и выбранного вида цен. Проблема заключается в том, что предложенный подход с вложенными циклами и соединением регистра с табличной частью документа может быть крайне неэффективным, особенно при большом количестве строк в табличной части.
Проанализируем исходный код и выявим его ключевые недостатки:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ПоступлениеТоваровИУслугТовары.Номенклатура КАК Номенклатура,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваровИУслуг.Товары КАК ПоступлениеТоваровИУслугТовары
| ПО ЦеныНоменклатурыСрезПоследних.Номенклатура = ПоступлениеТоваровИУслугТовары.Номенклатура";
Запрос.УстановитьПараметр("ВидЦены", Объект.ВидЦены);
Выборка = Запрос.Выполнить.Выбрать;
Пока Выборка.Следующий Цикл
// Пройти по строкам табличной части
Для Каждого Строка Из Объект.Товары Цикл
// Найти строку с соответствующей номенклатурой
Если Строка.Номенклатура = Выборка.Номенклатура Тогда
// Установить цену для найденной строки
Строка.Цена = Выборка.Цена;
//Прервать;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Основные недостатки этого подхода:
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПоступлениеТоваровИУслуг.Товары напрямую в запросе к регистру цен не имеет смысла, если документ ещё не записан или если мы хотим получить цены для номенклатуры, которая уже есть в табличной части, а не для всех возможных товаров. Если документ уже записан, этот запрос будет брать номенклатуру из записанных строк. Если же документ новый, то эта таблица будет пуста, и запрос не вернет нужных данных. Более того, запрос фактически выполняет соединение всех возможных цен со всеми товарами из существующего документа (если он уже сохранен), а не только с теми, что есть в текущей редактируемой табличной части.Пока Выборка.Следующий Цикл) и внутри него — цикл по строкам табличной части документа (Для Каждого Строка Из Объект.Товары Цикл). Это приводит к квадратичной сложности алгоритма (O(N*M), где N – количество цен, M – количество строк в табличной части). При большом количестве строк такая операция будет выполняться очень долго.Если Строка.Номенклатура = Выборка.Номенклатура Тогда).Для решения этой задачи мы предлагаем несколько более эффективных подходов, начиная от самых простых улучшений и заканчивая наиболее оптимальными.
Если по каким-то причинам мы вынуждены использовать обход вложенными циклами (например, если нет возможности передать табличную часть в запрос или она очень мала), то обязательно следует оптимизировать поиск во внутреннем цикле. Вместо ручного перебора строк и сравнения номенклатуры, мы можем использовать встроенный метод табличной части Найти() или НайтиСтроки().
Метод Найти() ищет первую строку, удовлетворяющую заданному условию, а НайтиСтроки() возвращает массив всех строк. Поскольку номенклатура в табличной части, как правило, уникальна или нам нужна первая подходящая строка, Найти() будет более подходящим.
Метод Найти() позволяет найти строку в табличной части по значению одного или нескольких ее реквизитов. Это значительно быстрее, чем ручной перебор, так как метод реализован на более низком уровне и оптимизирован для поиска.
Рассмотрим пример кода с применением Найти():
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
| ГДЕ ЦеныНоменклатурыСрезПоследних.Номенклатура В (&СписокНоменклатуры)"; // Добавим условие по номенклатуре
Запрос.УстановитьПараметр("ВидЦены", Объект.ВидЦены);
// Создадим список номенклатуры из табличной части для передачи в запрос
СписокНоменклатуры = Новый СписокЗначений;
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
СписокНоменклатуры.Добавить(СтрокаТЧ.Номенклатура);
КонецЦикла;
Запрос.УстановитьПараметр("СписокНоменклатуры", СписокНоменклатуры);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Ищем строку в табличной части по найденной номенклатуре
СтрокаТЧ = Объект.Товары.Найти(Выборка.Номенклатура, "Номенклатура");
Если СтрокаТЧ <> Неопределено Тогда
// Устанавливаем цену
СтрокаТЧ.Цена = Выборка.Цена;
КонецЕсли;
КонецЦикла;
Преимущества этого подхода:
Найти() выполняется значительно быстрее, чем ручной перебор, особенно для больших табличных частей.ГДЕ ЦеныНоменклатурыСрезПоследних.Номенклатура В (&СписокНоменклатуры), что позволяет получить цены только для той номенклатуры, которая фактически присутствует в нашей табличной части, а не для всей номенклатуры регистра цен.Недостатки:
Найти() в цикле по выборке. Для очень больших табличных частей это все еще может быть не самым оптимальным решением.Наиболее производительным и правильным подходом для таких задач является использование механизма временных таблиц в запросах. Этот подход позволяет передать данные из табличной части документа на сервер, обработать их там с помощью запроса (соединив с регистром цен) и получить уже готовые данные, содержащие цены для каждой номенклатуры.
Мы будем создавать временную таблицу, заполняя ее номенклатурой из табличной части документа, а затем использовать эту временную таблицу в основном запросе.
ТаблицаЗначений или Массив и передавать их в запрос как параметр. Однако, более производительно будет использовать конструктор Из и передавать коллекцию.Объект.Товары. Мы можем преобразовать эту табличную часть в ТаблицаЗначений и передать как параметр запроса.Найти(), как в предыдущем решении.Разберем пример кода для данного подхода:
// 1. Создаем менеджер временных таблиц
МВТ = Новый МенеджерВременныхТаблиц;
// 2. Создаем и заполняем временную таблицу из табличной части документа
// Для этого можно выгрузить табличную часть в ТаблицуЗначений
ТаблицаНоменклатуры = Объект.Товары.Выгрузить("Номенклатура"); // Получаем таблицу значений с одной колонкой "Номенклатура"
ЗапросВТ = Новый Запрос;
ЗапросВТ.МенеджерВременныхТаблиц = МВТ;
ЗапросВТ.Текст =
"ВЫБРАТЬ
| ТЗНоменклатура.Номенклатура КАК Номенклатура,
| СТРОКА(ТЗНоменклатура.Номенклатура) КАК СсылкаНаНоменклатуру // Можно добавить для удобства отладки
| ПОМЕСТИТЬ ВТ_НоменклатураДокумента
|ИЗ
| &ТаблицаНоменклатуры КАК ТЗНоменклатура";
ЗапросВТ.УстановитьПараметр("ТаблицаНоменклатуры", ТаблицаНоменклатуры);
ЗапросВТ.Выполнить(); // Выполняем запрос для создания временной таблицы
// 3. Основной запрос с соединением временной таблицы и регистра цен
ЗапросОсновной = Новый Запрос;
ЗапросОсновной.МенеджерВременныхТаблиц = МВТ; // Используем тот же МВТ
ЗапросОсновной.Текст =
"ВЫБРАТЬ
| ВТ_НоменклатураДокумента.Номенклатура КАК Номенклатура,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| ВТ_НоменклатураДокумента КАК ВТ_НоменклатураДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
| ПО ВТ_НоменклатураДокумента.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура";
ЗапросОсновной.УстановитьПараметр("ВидЦены", Объект.ВидЦены);
ВыборкаЦен = ЗапросОсновной.Выполнить().Выбрать();
// 4. Обход результата запроса и заполнение табличной части документа
Пока ВыборкаЦен.Следующий() Цикл
СтрокаТЧ = Объект.Товары.Найти(ВыборкаЦен.Номенклатура, "Номенклатура");
Если СтрокаТЧ <> Неопределено Тогда
СтрокаТЧ.Цена = ВыборкаЦен.Цена;
КонецЕсли;
КонецЦикла;
Преимущества этого подхода:
Чтобы избежать многократного вызова метода Найти() в цикле после выполнения основного запроса (что может быть неоптимально для очень больших табличных частей, хотя и лучше, чем ручной перебор), мы можем модифицировать временную таблицу. Вместо одной колонки "Номенклатура", мы можем добавить колонку с индексом строки или ссылкой на саму строку табличной части (если это возможно и безопасно).
Однако, передавать саму строку табличной части как параметр в запрос напрямую невозможно. Вместо этого мы можем передать уникальный идентификатор строки или, что чаще, номер строки вместе с номенклатурой. Тогда запрос вернет Номенклатуру, НомерСтроки и Цену. После этого мы можем обратиться к строке табличной части по номеру (Объект.Товары[НомерСтроки - 1]).
Рассмотрим пример такого подхода:
// 1. Создаем менеджер временных таблиц
МВТ = Новый МенеджерВременныхТаблиц;
// 2. Создаем ТаблицуЗначений с Номенклатурой и НомеромСтроки
ТаблицаНоменклатурыСНомерами = Новый ТаблицаЗначений;
ТаблицаНоменклатурыСНомерами.Колонки.Добавить("Номенклатура");
ТаблицаНоменклатурыСНомерами.Колонки.Добавить("НомерСтроки", Новый ОписаниеТипов("Число"));
ИндексСтроки = 0;
Для Каждого СтрокаТЧ Из Объект.Товары Цикл
НоваяСтрока = ТаблицаНоменклатурыСНомерами.Добавить();
НоваяСтрока.Номенклатура = СтрокаТЧ.Номенклатура;
НоваяСтрока.НомерСтроки = ИндексСтроки; // Нумерация с 0 для массива или с 1 для табличной части
ИндексСтроки = ИндексСтроки + 1;
КонецЦикла;
ЗапросВТ = Новый Запрос;
ЗапросВТ.МенеджерВременныхТаблиц = МВТ;
ЗапросВТ.Текст =
"ВЫБРАТЬ
| ТЗНоменклатура.Номенклатура КАК Номенклатура,
| ТЗНоменклатура.НомерСтроки КАК НомерСтроки
| ПОМЕСТИТЬ ВТ_НоменклатураДокумента
|ИЗ
| &ТаблицаНоменклатуры КАК ТЗНоменклатура";
ЗапросВТ.УстановитьПараметр("ТаблицаНоменклатуры", ТаблицаНоменклатурыСНомерами);
ЗапросВТ.Выполнить();
// 3. Основной запрос с соединением
ЗапросОсновной = Новый Запрос;
ЗапросОсновной.МенеджерВременныхТаблиц = МВТ;
ЗапросОсновной.Текст =
"ВЫБРАТЬ
| ВТ_НоменклатураДокумента.Номенклатура КАК Номенклатура,
| ВТ_НоменклатураДокумента.НомерСтроки КАК НомерСтроки,
| ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена
|ИЗ
| ВТ_НоменклатураДокумента КАК ВТ_НоменклатураДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
| ПО ВТ_НоменклатураДокумента.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура";
ЗапросОсновной.УстановитьПараметр("ВидЦены", Объект.ВидЦены);
ВыборкаЦен = ЗапросОсновной.Выполнить().Выбрать();
// 4. Обход результата запроса и заполнение табличной части
Пока ВыборкаЦен.Следующий() Цикл
Если ВыборкаЦен.НомерСтроки < Объект.Товары.Количество() Тогда // Проверяем, что индекс актуален
СтрокаТЧ = Объект.Товары[ВыборкаЦен.НомерСтроки]; // Прямое обращение по индексу
СтрокаТЧ.Цена = ВыборкаЦен.Цена;
КонецЕсли;
КонецЦикла;
Примечание: Если в табличной части могут быть повторяющиеся номенклатуры, и мы хотим заполнить цены для всех из них, то метод Найти() найдет только первую. В этом случае, если нам нужно обработать все строки, метод НайтиСтроки() вернет массив строк, который можно обойти. Однако, более правильный подход с временными таблицами, если в ВТ_НоменклатураДокумента мы будем передавать не только Номенклатуру, но и УникальныйИдентификаторСтрокиТЧ (если такой реквизит есть) или НомерСтроки. В примере выше с НомерСтроки мы гарантируем заполнение именно нужной строки. Если же номенклатура повторяется и цены для всех одинаковые, можно обойтись и Найти().
Перед тем как писать собственный код, всегда важно проверить, не реализован ли уже подобный функционал в типовой конфигурации. Многие стандартные конфигурации 1С (Управление Торговлей, Комплексная Автоматизация, ERP) имеют развитые механизмы работы с ценами.
Использование типового функционала всегда предпочтительнее собственной разработки, так как это снижает затраты на поддержку и обновление.
Для достижения максимальной производительности при работе с 1С и запросами, всегда обращайте внимание на следующие аспекты:
Номенклатура, ВидЦены в регистре цен), имеют индексы в базе данных. 1С обычно сама создает индексы для ключевых полей, но для нестандартных условий это может потребовать анализа.СрезПоследних), избегайте условий типа ВидЦены = &ВидЦены, где &ВидЦены является ссылкой на объект, а не его идентификатором. Если ВидЦены в регистре имеет составной тип, а &ВидЦены — конкретный тип, может потребоваться явное приведение типа с помощью ВЫРАЗИТЬ, чтобы СУБД могла эффективно использовать индексы. Однако, для обычных ссылочных типов это обычно не проблема.МенеджерВременныхТаблиц, когда нужно передать результаты одного запроса в другой или создать последовательность запросов, работающих с одними и теми же временными данными. Это намного эффективнее, чем создавать промежуточные ТаблицыЗначений в коде 1С и передавать их как параметры.ГДЕ и ПЕРИОД в виртуальных таблицах для минимизации объема обрабатываемых данных.Мы рассмотрели различные подходы к заполнению табличной части документа ценами в 1С. Исходный код автора, хотя и выполняет свою задачу, является крайне неэффективным из-за вложенных циклов и неоптимизированного поиска. Мы предложили следующие улучшения:
Объект.Товары.Найти() вместо ручного перебора для оптимизации поиска внутри цикла. Этот подход является базовым улучшением.Выбор конкретного решения зависит от размера табличной части, требований к производительности и сложности вашей задачи. Для большинства случаев, особенно с большими объемами данных, использование временных таблиц является предпочтительным и соответствует лучшим практикам разработки в 1С.