Как оптимизировать производительность табличной части и избежать зависаний при выводе динамических данных в 1С?

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

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

В этой статье мы подробно проанализируем классическую техническую проблему, связанную с динамическим выводом данных в табличную часть (ТЧ) документа на обычных формах (платформа 1С 8.2 / 8.3), разберем критические ошибки проектирования и рассмотрим правильные, оптимизированные алгоритмы решения задачи.

В чем суть проблемы: синдром «N+1 запроса» и зацикливание интерфейса

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

Попытка решить эту задачу «в лоб» обычно выглядит так: разработчик использует интерфейсное событие табличного поля ПриВыводеСтроки. Внутри этого события для каждой строки выполняется запрос к регистру сведений СрезПоследних по текущим номенклатуре и серии, после чего полученное значение подставляется в строку. Более того, чтобы «сохранить» актуальные данные, в этом же событии вызывается метод ЭтотОбъект.Записать().

Такой подход приводит к двум катастрофическим последствиям:

  1. Проблема N+1 запроса в событии ПриВыводеСтроки. Данное событие вызывается платформой для каждой отображаемой на экране строки табличного поля. Если на экране одновременно видны 30 строк, система выполнит 30 отдельных запросов к СУБД. При прокрутке (скроллинге) списка события будут генерироваться непрерывно, нагружая сервер тысячами мелких запросов. Интерфейс мгновенно начинает виснуть.
  2. Бесконечная рекурсия при вызове Записать(). Метод Записать() производит запись объекта в базу данных. Запись объекта приводит к обновлению данных формы на клиенте. Обновление формы вызывает перерисовку табличной части, что снова инициирует событие отрисовки строк. Происходит бесконечный цикл перезаписи документа, который намертво блокирует таблицы СУБД и приводит к аварийному завершению работы пользователей.

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

---

Архитектурное проектирование данных: Статический реквизит против динамического статуса

Прежде чем писать код, необходимо определиться с бизнес-логикой поля Способ. Зададимся вопросом: должен ли этот статус сохраняться в историческом документе «как было на момент проведения», или он всегда должен отображать оперативное состояние дел на «сегодня»?

Вариант А. «Способ» — это историческая часть хозяйственной операции. Если документ, проведенный в прошлом месяце, должен хранить именно тот способ, который действовал в момент его создания (даже если сегодня настройки изменились), то это значение должно быть полноценным реквизитом табличной части документа. Оно заполняется один раз при интерактивных действиях пользователя (выбор номенклатуры, подбор позиций) и сохраняется вместе с документом. При открытии старых документов обращаться к регистру не нужно — данные просто считываются из табличной части.

Вариант Б. «Способ» — это оперативный статус. Если документ старый, но пользователю всегда важно видеть актуальное на текущую секунду состояние из регистра, то этот реквизит вообще не должен принадлежать документу! Нам не нужно создавать реквизит в ТЧ документа. Достаточно добавить на форму *колонку табличного поля* (без привязки к данным) и заполнять ее динамически исключительно для отображения, без перезаписи самого документа.

Рассмотрим детальные технические решения для обоих вариантов.

---

Решение 1. Динамическое отображение через событие ПриПолученииДанных

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

Проанализируем по шагам правильный алгоритм работы в этом обработчике:

  1. Сбор параметров. Мы обходим переданную коллекцию ОформленияСтрок (в ней содержатся только видимые на экране строки) и собираем уникальные пары Номенклатура + Серия во временную таблицу значений.
  2. Один пакетный запрос. Передаем таблицу параметров в запрос. В запросе помещаем её во временную таблицу и соединяем с регистром сведений. Мы делаем всего ОДИН запрос к СУБД вместо десятков.
  3. Кэширование результата. Чтобы быстро сопоставить результаты запроса со строками на клиенте, мы выгружаем результат в быстрое Соответствие (кэш), где ключом будет уникальный строковый идентификатор связки. Встроенный тип Соответствие работает по принципу хэш-таблицы, поиск в нем происходит за сверхбыстрое время O(1).
  4. Оформление ячеек. Мы повторно обходим ОформленияСтрок, быстро достаем готовое значение из кэша по ключу и устанавливаем его методом УстановитьТекст(). Обратите внимание: мы меняем только визуальный текст оформления ячейки, не затрагивая сами данные документа и не вызывая никаких транзакций записи!

Рассмотрим подробный пример реализации этого метода:


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

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

---

Решение 2. Статическое заполнение «по изменению» (Вариант А)

Если бизнес-логика требует жесткого сохранения статуса в документе, то динамический опрос регистра при каждом выводе строки вреден. В этом случае мы должны заполнять колонку Способ (которая теперь является реквизитом ТЧ) только в моменты непосредственного изменения ключевых полей.

Выясним основные точки изменения условий:

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

Для первой ситуации мы задействуем обработчики событий элементов управления формы. Разберем пример реализации для события ПриИзменении:


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

// Обработчик изменения номенклатуры в строке
Процедура ТабличнаяЧастьНоменклатураПриИзменении(Элемент)
    ТекущаяСтрока = ЭлементыФормы.ТабличнаяЧасть.ТекущиеДанные;
    Если ТекущаяСтрока <> Неопределено Тогда
        ТекущаяСтрока.Способ = ПолучитьАктуальныйСпособ(ТекущаяСтрока.Номенклатура, ТекущаяСтрока.Серия);
    КонецЕсли;
КонецПроцедуры

// Обработчик изменения серии в строке
Процедура ТабличнаяЧастьСерияПриИзменении(Элемент)
    ТекущаяСтрока = ЭлементыФормы.TablichnayaChast.ТекущиеДанные;
    Если ТекущаяСтрока <> Неопределено Тогда
        ТекущаяСтрока.Способ = ПолучитьАктуальныйСпособ(ТекущаяСтрока.Номенклатура, ТекущаяСтрока.Серия);
    КонецЕсли;
КонецПроцедуры

Для второй ситуации (массовое добавление через подбор) необходимо осуществлять групповое заполнение реквизитов сразу после переноса данных в документ. Это делается в событии ОбработкаВыбора формы документа. Проанализируем пример пакетного обновления ТЧ после подбора:


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

В данном сценарии обновление ТЧ происходит мгновенно, незаметно для пользователя, и данные физически записываются в документ стандартным образом только тогда, когда пользователь нажимает кнопку «ОК» или «Записать» на форме документа.

---

Особенности оптимизации в Управляемых формах (платформа 1С 8.3)

Если вы планируете перевод конфигурации на управляемые формы (или уже ведете в них разработку), то принципы оптимизации изменятся. В управляемом интерфейсе у табличной части нет клиентских событий ПриВыводеСтроки и ПриПолученииДанных. Данные ТЧ передаются на клиент целиком в виде объекта ДанныеФормыКоллекция.

Для оптимизации управляемых форм рекомендуется применять следующие подходы:

---

Резюме и рекомендации разработчику

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

  1. Никогда не выполняйте запросы к базе данных в событии ПриВыводеСтроки. Если нужно вывести расчетные данные динамически — используйте только ПриПолученииДанных с обязательным кэшированием через Соответствие и пакетным запросом.
  2. Никогда не вызывайте запись объекта (Записать()) внутри интерфейсных событий вывода или получения данных. Это приводит к бесконечной рекурсии, взаимоблокировкам СУБД и искажению исторических данных документов.
  3. Разделяйте данные. Старайтесь не нагружать форму динамическими вычислениями, если эти данные можно один раз рассчитать и записать в реквизит ТЧ при интерактивном вводе строки пользователем.
  4. Соблюдайте принципы UX/UI. Обновляйте данные в табличной части тогда, когда изменились условия для их расчета (например, была изменена номенклатура), а не при каждом шевелении мыши пользователем.

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

← На главную