Как оптимизировать проведение документа и избавиться от неявных запросов в 1С?

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

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

В этой статье мы подробно разберем реальный пример неоптимального кода проведения документа, проанализируем каждую ошибку, выясним причины просадки производительности и пошагово проведем рефакторинг, используя лучшие практики разработки на платформе 1С v8.3.

Исходный код процедуры проведения

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


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    лкЗапрос = Новый Запрос;
    лкЗапрос.Текст = "ВЫБРАТЬ
                     |    РеализацияАвтомобилей.Автомобиль,
                     |    РеализацияАвтомобилей.Стоимость,
                     |    РеализацияАвтомобилей.СтоимостьУЕ,
                     |    РеализацияАвтомобилей.Ссылка
                     |ИЗ
                     |    Документ.РеализацияАвтомобилей.Автомобили КАК РеализацияАвтомобилей";
    
    лкВыборка = лкЗапрос.Выполнить().Выбрать();
    Пока лкВыборка.Следующий() Цикл
        
        лкЗапись = Движения.БУ.Добавить();
        
        лкЗапись.СчетДт         = лкВыборка.Ссылка.ДоговорВзаиморасчетов.СчетРасчетов;
        лкЗапись.СчетКт         = ПланыСчетов.БУ.Счет_90_01_1;
        
        лкЗапись.Период         = лкВыборка.Ссылка.Дата;
        лкЗапись.Регистратор    = лкВыборка.Ссылка;
        лкЗапись.Сумма          = лкВыборка.Стоимость;
        
        Если лкЗапись.СчетДт.Валютный Тогда
            лкЗапись.ВалютаДт = лкВыборка.Ссылка.ДоговорВзаиморасчетов.ВалютаУчета;
            лкЗапись.ВалютнаяСуммаДт = лкВыборка.СтоимостьУЕ;
        КонецЕсли;
        
        лкЗапись.Содержание = "Отражена вырузка по автомобилю " + лкВыборка.Автомобиль.Наименование;
        
        лкЗапись.СубконтоДт[ПланыВидовХарактеристик.ВидыСубконтоБУ.Контрагенты] = лкВыборка.Ссылка.Контрагент;
        лкЗапись.СубконтоДт[ПланыВидовХарактеристик.ВидыСубконтоБУ.ДоговорыВзаиморасчетов] = лкВыборка.Ссылка.ДоговорВзаиморасчетов;
        
        лкЗапись.СубконтоКт[ПланыВидовХарактеристик.ВидыСубконтоБУ.ВидыДеятельности] = лкВыборка.Ссылка.ВидДеятельности;
        лкЗапись.СубконтоКт[ПланыВидовХарактеристик.ВидыСубконтоБУ.Брэнды] = лкВыборка.Ссылка.Брэнд;
        лкЗапись.СубконтоКт[ПланыВидовХарактеристик.ВидыСубконтоБУ.СтавкиНДС] = лкВыборка.СтавкаНДС;
            
    КонецЦикла;    
КонецПроцедуры

На первый взгляд код кажется рабочим: движения формируются, субконто заполняются, проводки создаются. Однако с точки зрения производительности этот код таит в себе колоссальные проблемы.

---

Анализ проблем производительности и узких мест

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

1. Катастрофическое отсутствие отбора в запросе

Самая грубая и очевидная ошибка кроется в самом тексте запроса. Обратите внимание на секцию ИЗ:

Документ.РеализацияАвтомобилей.Автомобили КАК РеализацияАвтомобилей

В запросе полностью отсутствует секция ГДЕ с отбором по конкретному документу (Ссылка = &Ссылка). Это означает, что при проведении абсолютно любого документа система будет считывать из базы данных табличные части всех документов данного типа, когда-либо созданных в системе! Если в базе данных накопились десятки тысяч документов, этот запрос просто парализует работу СУБД, вызовет переполнение оперативной памяти и приведет к падению транзакции по таймауту блокировки.

2. Проблема неявных запросов в цикле (Разыменование ссылок через точку)

Рассмотрим следующую строку внутри цикла:

лкЗапись.СчетДт = лкВыборка.Ссылка.ДоговорВзаиморасчетов.СчетРасчетов;

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

Каждое использование «точки» от ссылочного поля в цикле генерирует дополнительное обращение к базе данных. В нашем примере внутри цикла таких обращений больше десятка на каждую итерацию! Если в табличной части документа 50 строк, система выполнит более 500 скрытых запросов к СУБД. Это классическая проблема производительности, известная как "N+1 запросов".

3. Неявный запрос при проверке признака «Валютный»

Конструкция:

Если лкЗапись.СчетДт.Валютный Тогда

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

4. Разыменование ссылки при получении наименования

Строка формирования содержания проводки:

лкЗапись.Содержание = "Отражена вырузка по автомобилю " + лкВыборка.Автомобиль.Наименование;

Здесь также происходит обращение к наименованию автомобиля через точку от ссылки, что порождает очередной неявный SQL-запрос для каждой строки табличной части.

5. Избыточные операции с реквизитами проведения

В цикле выполняются операции, которые не требуются при записи движений вручную:

---

Пошаговая оптимизация кода

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

Шаг 1. Перенос логики ветвления и разыменования в запрос

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

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


ВЫБРАТЬ
    РеализацияАвтомобилей.Автомобиль КАК Автомобиль,
    РеализацияАвтомобилей.Автомобиль.Наименование КАК АвтомобильНаименование,
    РеализацияАвтомобилей.Стоимость КАК Стоимость,
    РеализацияАвтомобилей.СтоимостьУЕ КАК СтоимостьУЕ,
    РеализацияАвтомобилей.СтавкаНДС КАК СтавкаНДС,
    // Получаем реквизиты шапки через Ссылка без неявных запросов
    РеализацияАвтомобилей.Ссылка.ДоговорВзаиморасчетов.СчетРасчетов КАК СчетДт,
    РеализацияАвтомобилей.Ссылка.ДоговорВзаиморасчетов.СчетРасчетов.Валютный КАК СчетДтВалютный,
    РеализацияАвтомобилей.Ссылка.ДоговорВзаиморасчетов.ВалютаУчета КАК ВалютаДт,
    РеализацияАвтомобилей.Ссылка.Контрагент КАК Контрагент,
    РеализацияАвтомобилей.Ссылка.ДоговорВзаиморасчетов КАК ДоговорВзаиморасчетов,
    РеализацияАвтомобилей.Ссылка.ВидДеятельности КАК ВидДеятельности,
    РеализацияАвтомобилей.Ссылка.Брэнд КАК Брэнд
ИЗ
    Документ.РеализацияАвтомобилей.Автомобили КАК РеализацияАвтомобилей
ГДЕ
    РеализацияАвтомобилей.Ссылка = &Ссылка

Обратите внимание: мы добавили условие ГДЕ РеализацияАвтомобилей.Ссылка = &Ссылка. Теперь запрос вернет строки табличной части только для проводимого в данный момент документа.

Шаг 2. Оптимизация работы с ПВХ (План Видов Характеристик)

В исходном коде обращения к видам субконто происходили через конструкции вида:

ПланыВидовХарактеристик.ВидыСубконтоБУ.Контрагенты

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

Шаг 3. Использование флага записи движений

Не забываем, что в процедуре проведения необходимо явно указывать системе, что набор движений модифицирован и его требуется записать в базу данных. Для этого используется свойство Движения.БУ.Записывать = Истина;.

---

Окончательный вариант оптимизированного кода

Объединим все наши архитектурные решения в итоговый программный код:


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

---

Альтернативный вариант: Использование метода Загрузить()

На форуме активно обсуждалась идея полного отказа от цикла с использованием метода Движения.БУ.Загрузить(ТаблицаЗначений). Рассмотрим этот подход подробнее.

Метод Загрузить() действительно работает быстрее, так как платформа выполняет запись набора данных в память одной транзакцией без интерпретации кода цикла на встроенном языке. Однако у этого способа есть несколько подводных камней, о которых важно помнить разработчику:

  1. Жесткое соответствие имен колонок. Имена колонок в таблице значений должны строго совпадать с именами реквизитов регистра. Если вы допустите опечатку в псевдониме поля в запросе, метод отработает некорректно или вызовет ошибку времени выполнения.
  2. Сложность работы с субконто. Чтобы загрузить субконто через этот метод, вам потребуется передавать в таблице значений не только сами значения субконто, но и типы (виды) субконто в строго определенных колонках (например, ВидСубконтоДт1, СубконтоДт1). Любое изменение порядка следования субконто в плане счетов пользователем в режиме 1С:Предприятие мгновенно сломает логику такой загрузки.
  3. Использование универсальных функций БСП. В реальных типовых конфигурациях (например, ERP, Комплексная автоматизация или Бухгалтерия предприятия) для заполнения субконто применяется специальный программный интерфейс, например БухгалтерскийУчет.УстановитьСубконто(). Данный метод динамически определяет правильный индекс аналитики на счете, что делает программу отказоустойчивой. Использование Загрузить() лишает вас этой гибкости.

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

---

Резюме правил оптимизации проведения в 1С

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

Применение этих простых правил гарантирует высокую скорость проведения документов и стабильную работу системы даже под экстремальными нагрузками.

← На главную