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

Программист IT и автоматизация бизнеса
← На главную

При разработке и доработке конфигураций на платформе 1С разработчики часто сталкиваются с ситуацией, когда процедура проведения или записи документа начинает сильно замедляться. При анализе замера производительности нередко выясняется, что львиную долю времени занимает, казалось бы, безобидная проверка условия в цикле по строкам табличной части — например, обращение к реквизиту номенклатуры через точку вида СтрокаТЧ.Номенклатура.ВидНоменклатуры. Разберем подробно, почему возникает эта проблема, что происходит внутри платформы и СУБД, а также рассмотрим правильные способы оптимизации данного участка кода.

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

Выясним причину такого поведения платформы. Когда мы пишем конструкцию СтрокаТЧ.Номенклатура.ВидНоменклатуры, мы обращаемся к реквизиту объекта через ссылку. Платформа 1С устроена так, что при разыменовании ссылки (обращении через точку к реквизиту ссылочного типа) происходит следующее:

  1. Чтение объекта целиком: Платформа не считывает только одно запрошенное поле ВидНоменклатуры. Вместо этого в память считывается вся строка таблицы справочника (все реквизиты объекта). Если объект содержит множество реквизитов, длинные строковые описания или хранилища значений, все эти данные передаются от СУБД на сервер 1С.
  2. Поведение кэша объектов: Считанные данные помещаются во внутренний кэш платформы. Если в табличной части несколько сотен строк с уникальными позициями, кэш быстро заполняется и вытесняется. В результате на каждой итерации цикла платформа вынуждена отправлять отдельный SQL-запрос к базе данных (классическая проблема $N+1$ запросов).
  3. Сетевые и транзакционные накладные расходы: Каждое такое обращение — это межпроцессный или сетевой обмен между сервером приложений 1С и СУБД, который в цикле на 200–500 строк может занимать до 30–50% общего времени выполнения операции.

Проанализируем типичную ошибочную попытку оптимизации. Часто разработчики пытаются заменить обращение через точку на запрос внутри самого цикла:


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

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

Решение 1. Получение данных одним запросом с использованием массива ссылок и структуры «Соответствие»

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

Разберем реализацию по шагам:

  1. Выгружаем массив или список ссылок из колонки табличной части с помощью метода ВыгрузитьКолонку().
  2. Передаем полученный массив в условие запроса ГДЕ Номенклатура.Ссылка В (&СписокНоменклатуры).
  3. Фильтруем или выбираем нужные реквизиты непосредственно на стороне СУБД.
  4. Помещаем результат выборки в быстрый объект коллекции памяти Соответствие, поиск по которому выполняется за $O(1)$.
  5. В основном цикле проверяем наличие ссылки в нашей подготовленной коллекции.

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


// 1. Формируем список ссылок номенклатуры из табличной части документа
МассивНоменклатуры = ДокОбъект.Товары.ВыгрузитьКолонку("Номенклатура");

// 2. Выполняем один запрос для получения позиций с нужным видом
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	Номенклатура.Ссылка КАК Номенклатура
    |ИЗ
    |	Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |	Номенклатура.Ссылка В(&МассивНоменклатуры)
    |	И Номенклатура.ВидНоменклатуры = ЗНАЧЕНИЕ(Перечисление.ВидыНоменклатуры.ПодарочныйСертификат)";

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

// 3. Заполняем соответствие для мгновенной проверки в памяти
Сертификаты = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    Сертификаты.Вставить(Выборка.Номенклатура, Истина);
КонецЦикла;

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

Важное замечание: Обратите внимание на использование объекта Соответствие вместо метода Массив.Найти(). Поиск по массиву выполняет линейный перебор элементов ($O(N)$), что при больших объемах строк вновь приведет к квадратичной сложности $O(N \times M)$. Поиск по ключу в Соответствие работает практически мгновенно благодаря хэшированию.

Решение 2. Использование временной таблицы для больших объемов данных

Если в документе содержится большое количество строк (более 1 000 – 2 000 позиций), передача огромного массива ссылок в параметр В (&СписокНоменклатуры) может перегрузить парсер SQL-запросов и привести к неоптимальному плану выполнения в СУБД. В таких случаях по стандартам разработки 1С рекомендуется использовать Временные Таблицы.

Разберем по шагам реализацию через временную таблицу:

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

Посмотрим на пример кода:


МенеджерВТ = Новый МенеджерВременныхТаблиц;

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

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

РезультатТаблица = Запрос.Выполнить().Выгрузить();
РезультатТаблица.Индексы.Добавить("Номенклатура");

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

Решение 3. Использование типовых функций БСП

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


// Получение значений реквизитов для массива ссылок за один пакетный запрос
МассивНоменклатуры = ДокОбъект.Товары.ВыгрузитьКолонку("Номенклатура");
ДанныеНоменклатуры = ОбщегоНазначения.ЗначенияРеквизитовОбъектов(МассивНоменклатуры, "ВидНоменклатуры");

Для Каждого СтрокаТЧ Из ДокОбъект.Товары Цикл
    СтруктураРеквизитов = ДанныеНоменклатуры[СтрокаТЧ.Номенклатура];
    Если СтруктураРеквизитов <> Неопределено 
       И СтруктураРеквизитов.ВидНоменклатуры <> Перечисления.ВидыНоменклатуры.ПодарочныйСертификат Тогда
        // Логика проверки
    КонецЕсли;
КонецЦикла;

Сравнительный анализ и выводы

Проанализируем результаты оптимизации:

  1. Замена разыменования на пакетный запрос: Сокращает время выполнения проверки с десятков процентов от общего времени транзакции до сотых долей процента ($< 0.1\%$).
  2. Минимизация нагрузки на СУБД: Вместо сотен одиночных запросов к базе выполняется ровно один запрос, выбирающий строго необходимые поля.
  3. Надежность: Исключаются проблемы с переполнением и вытеснением платформенного кэша объектов при обработке больших документов.

Придерживайтесь правила: любые проверки реквизитов ссылочных данных для табличных частей и списков должны выполняться пакетно до входа в цикл обработки данных.

← На главную