Как оптимизировать работу с большими табличными частями в документах 1С, когда объект еще не записан?

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

Мы сталкиваемся с распространенной проблемой: обработка документов с очень большим количеством строк в табличной части (более 5000) значительно замедляет работу системы. В частности, операции по выгрузке данных из табличной части для использования в запросах могут занимать неприемлемое время, например, 1-3 секунды на выгрузку одной колонки номенклатуры. Проблема становится особенно актуальной в обработчиках события "ПередЗаписью", когда объект еще не сохранен в базе данных, и мы не можем использовать прямую ссылку на записанный объект.

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

Понимание проблемы: почему "медленно"?

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


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

Здесь кроется ключевая причина низкой производительности. Мы сначала выгружаем всю табличную часть источник.товары целиком в промежуточную таблицу значений со всеми ее колонками (метод Выгрузить()), а уже затем из этой промежуточной таблицы значений выбираем нужную нам колонку (метод ВыгрузитьКолонку("Номенклатура")). Для табличной части, содержащей 5000+ строк и, возможно, множество реквизитов, создание такой полной промежуточной таблицы значений является очень ресурсоемкой операцией. Это подтверждается экспериментами:

  1. Выгрузка одной колонки напрямую из ТЧ (например, ТЧ.ВыгрузитьКолонку("Реквизит1")) может занимать около 0,001 секунды.
  2. Выгрузка всей ТЧ (ТЧ.Выгрузить()) может занимать около 0,05 секунды.
  3. Последующая выгрузка колонки из полной таблицы значений (ТЗ.ВыгрузитьКолонку("Реквизит1")) может занимать около 0.0002 секунды.
  4. Выгрузка в таблицу значений с указанием одной колонки (ТЧ.Выгрузить(,"Реквизит1")) может занимать около 0,007 секунды.

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

Метод 1: Прямое использование метода ВыгрузитьКолонку() для табличной части

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

Пример использования:


Номенклатура = источник.Товары.ВыгрузитьКолонку("Номенклатура");

Этот подход значительно быстрее исходного, поскольку мы сразу получаем необходимый массив значений, не затрачивая ресурсы на создание полной таблицы значений. По нашим тестам, эта операция может занять всего около 0,001 секунды для 5000 строк, что является колоссальным улучшением по сравнению с 1-3 секундами, которые мог занимать первоначальный подход.

Важный нюанс для форм: если вы работаете с табличной частью, которая является ДанныеФормыКоллекция (например, когда данные берутся напрямую из элементов формы), метод ВыгрузитьКолонку() может быть недоступен напрямую. В этом случае вам сначала потребуется преобразовать ДанныеФормыКоллекция в обычную ТаблицаЗначений с помощью функции ДанныеФормыВЗначение(), а затем уже применять ВыгрузитьКолонку():


// Предположим, что ТаблицаТоваровФормы - это элемент формы типа ДанныеФормыКоллекция
ТаблицаЗначенийТовары = ДанныеФормыВЗначение(ТаблицаТоваровФормы, Тип("ТаблицаЗначений"));
Номенклатура = ТаблицаЗначенийТовары.ВыгрузитьКолонку("Номенклатура");

Также, если у вас есть общая функция, как например ОбщегоНазначения.ВыгрузитьКолонку(), которая реализует аналогичную логику, вы можете использовать ее:


Номенклатура = ОбщегоНазначения.ВыгрузитьКолонку(источник.Товары, "Номенклатура", Истина);

Метод 2: Использование метода Выгрузить() с указанием конкретных колонок

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

Пример использования:


// Выгружаем только колонку "Номенклатура" и "Количество" в таблицу значений
ТЗ_НоменклатураКоличество = источник.Товары.Выгрузить(, "Номенклатура,Количество");

// Затем, если нужен массив номенклатуры из этой ТЗ:
МассивНоменклатуры = ТЗ_НоменклатураКоличество.ВыгрузитьКолонку("Номенклатура");

Этот подход также более эффективен, чем полная выгрузка, и может занять около 0,007 секунды для 5000 строк при выгрузке одной колонки.

Метод 3: Передача данных в запрос через временные таблицы

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

Шаг 1: Подготовка таблицы значений с явно типизированными колонками

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


// Создаем таблицу значений для передачи в запрос
ПередаваемаяТЗ = Новый ТаблицаЗначений;

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

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

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


// Если нужно передать часть табличной части
// В этом случае платформы сама определит типы, но явное указание все равно предпочтительнее для сложных типов
ПередаваемаяТЗ = источник.Товары.Выгрузить(, "Номенклатура, Количество");

Шаг 2: Создание временной таблицы в запросе

После подготовки ТаблицаЗначений мы можем передать ее в запрос и использовать для создания временной таблицы. В тексте запроса это делается с помощью ключевого слова ПОМЕСТИТЬ.


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

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

Результат = Запрос.Выполнить().Выгрузить();

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

Менеджер временных таблиц (МВТ):

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


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

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

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

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

Особенности работы в событиях "ПередЗаписью" и "ПриЗаписи"

Автор упомянул, что код выполняется в событии "ПередЗаписью" и данные в источник.ссылка могут отличаться от данных в объекте. Разберем эту ситуацию подробнее:

  1. "ПередЗаписью": Это событие вызывается до фактической записи объекта в базу данных. В нем вы работаете с текущим состоянием объекта в памяти. Объект.Ссылка (или источник.Ссылка, если это ссылка из внешнего источника) будет содержать либо пустую ссылку для нового объекта, либо ссылку на старую версию объекта, если он уже был записан ранее. Важно: в "ПередЗаписью" еще можно отменить запись объекта, установив параметр Отказ в Истина. Это событие идеально подходит для проверок и предварительных расчетов, которые должны предшествовать записи.
  2. "ПриЗаписи": Это событие вызывается после того, как данные объекта уже записаны в базу данных, но до закрытия транзакции. В этом обработчике ТекущийОбъект.Ссылка (или Объект.Ссылка) будет содержать уже актуальную, записанную в базу ссылку на элемент, даже если это новый объект. "ПриЗаписи" подходит для выполнения действий, связанных с дополнительными данными, которые должны быть синхронизированы с основным объектом, но которые не влияют на его непосредственную запись (например, движения регистров, создание связанных объектов). Если в вашем случае обработка данных не требует возможности отмены записи и может быть выполнена после получения актуальной ссылки, перенос логики в "ПриЗаписи" может упростить работу с запросами, позволяя передавать ссылку на документ, а не всю его табличную часть.

Общие рекомендации по оптимизации производительности в 1С

Помимо рассмотренных методов, давайте вспомним общие принципы оптимизации, которые всегда полезны при работе с 1С:

  1. Оптимизация запросов:
    • Минимизируйте объем выборки, выбирая только те поля, которые действительно необходимы.
    • Избегайте сложных запросов, по возможности разбивайте их на более мелкие и управляемые части.
    • Используйте индексы для ускорения поиска и сортировки данных в базе данных.
    • Старайтесь избегать неявных запросов (обращение к реквизитам ссылки через точку в цикле), вместо этого выполните один общий запрос и работайте с полученными данными в памяти.
    • Откажитесь от логического "ИЛИ", использования подзапросов при соединении и получения данных через точку без ключевого слова "ВЫРАЗИТЬ", а также соединения обычных и виртуальных таблиц без предварительного помещения виртуальной таблицы во временную с индексами.
  2. Кэширование данных: Для часто используемых, но редко изменяющихся данных рассмотрите возможность их кэширования в памяти на уровне приложения или базы данных.
  3. Параллельная обработка: Если задача допускает, разделите данные на части и обрабатывайте их параллельно, используя многопоточность или асинхронную обработку.
  4. Настройка сервера и базы данных: Убедитесь, что ваш сервер 1С и СУБД (SQL Server, PostgreSQL и т.д.) правильно настроены, имеют достаточно ресурсов (CPU, RAM, быстрые диски), а программное обеспечение и драйверы регулярно обновляются.
  5. Свертка информационной базы: Для баз с очень большим историческим объемом данных свертка может значительно сократить объем рабочей базы и повысить общую скорость работы системы.
  6. Клиент-серверный вариант работы: Переход на клиент-серверный вариант работы позволяет распределить нагрузку между клиентом и сервером, что критически важно для производительности в крупных системах.
  7. Консоль запросов: Всегда используйте консоль запросов для разработки и тестирования ваших запросов. Она позволяет просматривать содержимое временных таблиц, замерять время выполнения пакетов запросов и выявлять узкие места.

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

← На главную