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