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

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

При разработке и поддержке конфигураций на платформе 1С:Предприятие программисты часто сталкиваются с кодом вида Заказ.Договор.Владелец.ИНН. Подобные конструкции, называемые разыменованием ссылок, кажутся простыми и удобными, однако они таят в себе серьезную угрозу для производительности всей системы. Одиночное обращение через три точки может выполняться незаметно, но когда такие проверки запускаются массово или внутри циклов, это забирает до 40% и более процессорного времени. В этой статье мы подробно проанализируем, почему этот механизм работает медленно, что происходит в этот момент «под капотом» платформы и СУБД, а также рассмотрим практические методы оптимизации кода.

Что происходит «под капотом» платформы при обращении через точку?

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

  1. Переход от табличной модели к объектной. При обращении к реквизиту по ссылке платформа 1С не делает точечный SQL-запрос для получения одного конкретного поля. Вместо этого она считывает из базы данных весь объект целиком со всеми его реквизитами шапки (а иногда и табличными частями, если они кэшируются). Получается, что ради проверки одного лишь ИНН система прокачивает через сеть мегабайты лишних данных.
  2. Засорение кэша объектов. Считанный тяжелый объект помещается в кэш на сервере приложений 1С. Если в коде происходит обход сотен различных документов, кэш быстро переполняется, вытесняя действительно нужные данные, что приводит к лавинообразному росту дисковых операций чтения на сервере СУБД.
  3. Проблема составных типов данных. Это наиболее критичный аспект. Если промежуточный реквизит в цепочке (например, Владелец в справочнике договоров) имеет составной тип данных, СУБД не знает заранее, в какой именно таблице хранятся данные. В результате на уровне СУБД строится динамический SQL-запрос, содержащий левые соединения (LEFT JOIN) со всеми физическими таблицами, входящими в составной тип. Если составной тип включает 15–20 документов или справочников, СУБД выполнит тяжелейшее соединение всех этих таблиц, что полностью ломает план оптимизатора запросов.
  4. Сетевой пинг (Roundtrips) на клиенте. Если код выполняется в контексте &НаКлиенте, то каждое обращение через точку к реквизиту ссылки порождает неявный синхронный вызов сервера. Цепочка из трех точек превращается в несколько последовательных сетевых запросов туда и обратно. На медленных каналах связи или VPN-соединениях это вызывает полное зависание интерфейса пользователя.
  5. Транзакционные блокировки. Объектный метод чтения данных внутри транзакции может приводить к наложению разделяемых блокировок (S-блокировок) на всю запись таблицы СУБД, снижая параллельность работы пользователей и увеличивая частоту возникновения взаимоблокировок (Deadlocks).

Рассмотрим по шагам основные способы борьбы с этой проблемой.

Способ 1. Использование локальных переменных для кэширования данных

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


Если ЗначениеЗаполнено(Заказ.Договор) Тогда
    ДоговорДата = Заказ.Договор.Дата;
    ДоговорНомер = Заказ.Договор.Номер;
    КонтрагентНаименование = Заказ.Договор.Владелец.Наименование;
    КонтрагентИНН = Заказ.Договор.Владелец.ИНН;
КонецЕсли;

В данном фрагменте платформа выполнит разыменование Заказ.Договор четыре раза, а разыменование Договор.Владелец — два раза. Суммарно СУБД получит избыточную нагрузку. Перепишем этот код правильно, сохранив промежуточные ссылки в локальные переменные:


ДоговорСсылка = Заказ.Договор;
Если ЗначениеЗаполнено(ДоговорСсылка) Тогда
    
    // Считываем свойства договора один раз
    ДоговорДата = ДоговорСсылка.Дата;
    ДоговорНомер = ДоговорСсылка.Номер;
    
    // Переходим к владельцу договора
    КонтрагентСсылка = ДоговорСсылка.Владелец;
    Если ЗначениеЗаполнено(КонтрагентСсылка) Тогда
        КонтрагентНаименование = КонтрагентСсылка.Наименование;
        КонтрагентИНН = КонтрагентСсылка.ИНН;
    КонецЕсли;
    
КонецЕсли;

В оптимизированном варианте количество обращений сократилось более чем в два раза. Мы сохранили ссылки во временные переменные ДоговорСсылка и КонтрагентСсылка, минимизировав количество неявных запросов к базе данных.

Способ 2. Использование функций Библиотеки Стандартных Подсистем (БСП)

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

Для получения одного реквизита используем метод ЗначениеРеквизитаОбъекта:


// Получаем только ИНН владельца договора через БСП
ИНН = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Заказ.Договор.Владелец, "ИНН");

Если требуется получить несколько реквизитов, в том числе через цепочку точек (разыменование), идеальным решением будет использование метода ЗначенияРеквизитовОбъекта, передав структуру путей к реквизитам. Разберем этот пример:


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

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

// Извлекаем полученные значения
КонтрагентИНН = Результат.ИНН;
КонтрагентНаименование = Результат.Наименование;
НомерДоговора = Результат.НомерДоговора;

Этот подход гарантирует, что платформа выполнит ровно один оптимальный запрос к СУБД с явным указанием выбираемых полей (SELECT), без загрузки тяжелых объектов в оперативную память.

Способ 3. Перевод логики на запросы

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

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


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

// Исключаем дубликаты
ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивДоговоров);

Если МассивДоговоров.Количество() > 0 Тогда

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

КонецЕсли;

Такой подход обеспечивает колоссальный прирост производительности на больших объемах данных, так как вместо сотен обращений к СУБД выполняется всего один оптимизированный запрос.

Способ 4. Использование функций повторного использования (Кэширование)

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


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

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

Способ 5. Денормализация структуры данных (Архитектурное решение)

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

Если для бизнес-логики документа Заказ критически важно постоянно анализировать или выводить в списки ИНН контрагента, этот ИНН (или сам Контрагент напрямую, минуя Договор) должен быть добавлен в качестве физического реквизита самого документа Заказ:

Резюме и рекомендации

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

← На главную