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

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

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

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

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


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

В этом примере на каждую строку документа система делает отдельное обращение к базе данных. Если в документе 100 строк, сервер выполнит 100 запросов. Это создает колоссальную нагрузку на SQL-сервер и значительно замедляет проведение документа. Проанализируем ситуацию: наша цель — получить все данные одним "выстрелом" и обработать их разом.

Решение №1: Использование одного запроса с левым соединением

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


Процедура ПроверкаЦен(Отказ)
    Если Отказ Тогда Возврат; КонецЕсли;

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

    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    Запрос.УстановитьПараметр("Период", Дата);
    Запрос.УстановитьПараметр("ТипЦен", ТипЦен);

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

Разберем, что мы сделали. Во-первых, мы использовали ЛЕВОЕ СОЕДИНЕНИЕ, чтобы сопоставить каждую строку документа с ценой из регистра. Во-вторых, в секции ГДЕ мы сразу отфильтровали только те строки, которые нарушают наше условие (Цена < ЦенаРегистра). Таким образом, выборка будет содержать только "проблемные" строки.

Решение №2: Оптимизация через временные таблицы

Если документ еще не записан в базу данных или вы хотите передать данные из ТЧ, которые были изменены программно перед проведением, эффективнее использовать временные таблицы (ВТ). Это позволяет SQL-серверу более эффективно строить план запроса.

Рассмотрим пошагово, как работать с временными таблицами в 1С:

  1. Выгружаем табличную часть документа в таблицу значений.
  2. Передаем эту таблицу в запрос как параметр.
  3. Внутри запроса помещаем данные во временную таблицу и индексируем поле Номенклатура.
  4. Соединяем временную таблицу с виртуальной таблицей регистра.

ТаблицаТоваров = Товары.Выгрузить();
МенеджерВТ = Новый МенеджерВременных Таблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;

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

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

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

Важные технические нюансы оптимизации

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

1. Проверка свойства ОбменДанными.Загрузка. Если ваш документ загружается из другой базы (через конвертацию данных или API), проверки при проведении могут только мешать. Всегда добавляйте в начало процедур проверки условие:

Если ОбменДанными.Загрузка Тогда Возврат; КонецЕсли;

2. Параметры виртуальных таблиц. Обратите внимание, что условие по ТипЦен мы передали в параметры виртуальной таблицы СрезПоследних, а не в секцию ГДЕ. Проанализируем ситуацию: если передать условие в ГДЕ, система сначала получит цены для всех типов цен на указанную дату, и только потом отфильтрует их. Это крайне неэффективно для больших регистров.

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

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

5. Разделение логики. Выносите тяжелые проверки в Общий модуль с признаком "Сервер". Это позволит вам переиспользовать этот же код, например, в форме документа при нажатии кнопки "Проверить цены" или перед записью документа.

Подведем итоги

Мы выяснили, что основной враг производительности в 1С — это обращение к базе данных внутри циклов. Для оптимизации проведения документа Заказ покупателя мы рассмотрели:

Следуя этим правилам, вы обеспечите высокую скорость работы системы даже при очень больших объемах данных в документах и справочниках.

← На главную