Как ускорить запись результата запроса в регистр сведений

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

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

В чем причина низкой скорости стандартного метода?

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


Пока Выборка.Следующий() Цикл
    НаборЗапись = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьМенеджерЗаписи();
    НаборЗапись.Период = Выборка.Period;
    // ... заполнение остальных реквизитов и измерений ...
    НаборЗапись.Записать(Истина);
КонецЦикла;

Этот подход содержит в себе сразу несколько критических проблем производительности:

  1. Избыточность операций СУБД (SELECT, DELETE, INSERT): При использовании объекта РегистрСведенийМенеджерЗаписи платформа «1С:Предприятие» выполняет под капотом три операции. Сначала она делает запрос к СУБД (SELECT), чтобы проверить, существует ли уже запись с такими ключевыми полями. Если запись существует, платформа выполняет ее удаление (DELETE), а затем вставляет новую строку (INSERT). Если записи нет, происходит только вставка. На объеме в 6 миллионов строк это превращается в 18 миллионов тяжелых операций с базой данных!
  2. Огромное количество неявных транзакций: Каждый вызов метода Записать() без явно открытой транзакции инициирует отдельную транзакцию на уровне СУБД. База данных вынуждена открывать транзакцию, записывать одну-единственную строку, фиксировать транзакцию и сбрасывать буферы на диск. Дисковая подсистема сервера при этом испытывает колоссальную нагрузку (очередь к диску растет, производительность падает до критического минимума).
  3. Высокие накладные расходы на создание объектов: Оператор СоздатьМенеджерЗаписи() вызывается на каждой итерации цикла. Платформа тратит процессорное время и оперативную память на выделение ресурсов под миллионы временных объектов.

Для решения этой проблемы необходимо кардинально изменить подход к работе с транзакциями и использовать специализированные объекты для массовой работы с данными — наборы записей.

---

Решение 1. Использование транзакций и пакетная запись порциями

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

Когда мы открываем транзакцию с помощью оператора НачатьТранзакцию(), СУБД выполняет запись данных в оперативную память (буферы) и в журнал транзакций во временном режиме. Физическая фиксация изменений на диске происходит только один раз — при вызове команды ЗафиксироватьТранзакцию(). Это позволяет сократить количество дисковых операций ввода-вывода (I/O) в тысячи раз.

Однако нельзя помещать все 6 миллионов строк в одну транзакцию. Слишком большая транзакция приведет к переполнению журнала транзакций СУБД, нехватке оперативной памяти на сервере приложений 1С и, что самое опасное, к блокировке всей таблицы регистра для других пользователей. Оптимальный размер пакета (порции) подбирается экспериментально и обычно составляет от 10 000 до 50 000 строк.

Рассмотрим пример реализации порционной записи в транзакциях с использованием объекта РегистрСведенийНаборЗаписей:


// Определяем размер пакета для записи
РазмерПакетнойЗаписи = 50000; 

Выборка = Запрос.Выполнить().Выбрать();
СчетчикСтрок = 0;

// Создаем один набор записей, который будем использовать повторно
НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();

НачатьТранзакцию();

Пока Выборка.Следующий() Цикл
    
    // Добавляем новую запись в текущий набор
    НоваяЗапись = НаборЗаписей.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяЗапись, Выборка);
    НоваяЗапись.Пользователь = Пользователи.ТекущийПользователь();
    
    СчетчикСтрок = СчетчикСтрок + 1;
    
    // Если размер пакета достигнут, производим запись и фиксируем транзакцию
    Если СчетчикСтрок % РазмерПакетнойЗаписи = 0 Тогда
        
        // Записываем набор без замещения существующих данных (параметр Ложь)
        НаборЗаписей.Записать(Ложь);
        ЗафиксироватьТранзакцию();
        
        // Очищаем набор записей для подготовки к следующему пакету
        НаборЗаписей.Очистить();
        
        // Начинаем новую транзакцию для следующей порции данных
        НачатьТранзакцию();
        
    КонецЕсли;
    
КонецЦикла;

// Не забываем записать остаток данных, который не вошел в последний полный пакет
Если НаборЗаписей.Количество() > 0 Тогда
    НаборЗаписей.Записать(Ложь);
КонецЕсли;

ЗафиксироватьТранзакцию();

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

---

Решение 2. Обход бизнес-логики через ОбменДанными.Загрузка

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

Для массовой технической заливки данных эту бизнес-логику необходимо временно отключить. В платформе 1С для этого предусмотрен стандартный механизм — свойство ОбменДанными.Загрузка. Давайте посмотрим, как правильно применить его при массовой записи наборов:


НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
// Установка признака загрузки данных
НаборЗаписей.ОбменДанными.Загрузка = Истина;

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


Процедура ПередЗаписи(Отказ, Замещение)
    Если ОбменДанными.Загрузка Тогда
        Возврат; // Прерываем выполнение обработчика для ускорения записи
    КонецЕсли;
    
    // Обычная бизнес-логика...
КонецПроцедуры

---

Решение 3. Использование метода Загрузить() для готовых таблиц

Если результат вашего запроса можно сразу выгрузить в таблицу значений, вы можете использовать метод Загрузить() объекта РегистрСведенийНаборЗаписей. Этот метод работает на уровне платформы чрезвычайно быстро, так как перенос данных из структуры ТаблицаЗначений в структуру набора записей оптимизирован на уровне C++ ядра платформы.

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


// Выгружаем результат запроса напрямую в Таблицу Значений
ТаблицаРезультата = Запрос.Выполнить().Выгрузить();

// Добавляем колонку "Пользователь", если ее не было в запросе
ТаблицаРезультата.Колонки.Добавить("Пользователь");
ТаблицаРезультата.ЗаполнитьЗначения(Пользователи.ТекущийПользователь(), "Пользователь");

// Создаем набор записей
НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
НаборЗаписей.ОбменДанными.Загрузка = Истина;

// Загружаем всю таблицу значений в набор
НаборЗаписей.Загрузить(ТаблицаРезультата);

// Записываем данные в базу (Внимание: этот метод подходит, если набор перезаписывает данные по отбору)
НаборЗаписей.Записать(Истина);

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

---

Решение 4. Новые возможности платформы: Режимы замещения (1С v8.3.25+)

В современных версиях платформы «1С:Предприятие» (начиная с 8.3.25) появился революционный инструмент для массовой работы с базами данных — расширенные режимы записи наборов с использованием системного перечисления РежимЗамещения. Ранее у метода Записать() был только булев параметр (Истина/Ложь), что сильно ограничивало возможности оптимизации операций на уровне СУБД.

Теперь мы можем передавать в метод Записать() специальные режимы, которые СУБД выполняет нативно и максимально быстро:

  1. РежимЗамещения.Добавление: Аналог быстрой вставки. Строки просто добавляются в конец таблицы без предварительных проверок. Идеально для пустых таблиц или гарантированно уникальных данных.
  2. РежимЗамещения.Слияние: Самый долгожданный режим (на уровне SQL это операция MERGE / UPSERT). Если строка с такими измерениями уже существует, СУБД обновляет ее ресурсы и реквизиты. Если строки нет — она просто добавляется. Это полностью решает проблему построчных проверок существования записей силами платформы!
  3. РежимЗамещения.Обновление: Производит только обновление существующих записей в базе данных. Если записи с такими измерениями нет, вставка не происходит. Это экономит ресурсы СУБД, так как операция UPDATE выполняется быстрее, чем INSERT.

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


НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
НаборЗаписей.ОбменДанными.Загрузка = Истина;

// Заполняем набор записей порцией данных...
// ...

// Записываем в режиме Слияния (доступно в 1С 8.3.25+)
НаборЗаписей.Записать(РежимЗамещения.Слияние);

---

Решение 5. Распараллеливание записи (Фоновые задания)

Если у сервера приложений 1С и сервера СУБД есть свободные вычислительные мощности (многоядерные процессоры, быстрые NVMe-накопители), запись 6 миллионов строк можно распараллелить. Вместо записи в один поток мы можем запустить, например, 8 или 16 параллельных фоновых заданий, каждое из которых будет записывать свою часть данных. Для реализации и контроля выполнения длительных процессов без БСП можно использовать пример работы с фоновыми заданиями.

Для успешного распараллеливания без возникновения взаимоблокировок (Deadlocks) необходимо строго соблюдать три правила:

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

  1. Основной сеанс выполняет тяжелый запрос и выгружает результат во временную таблицу или индексированную таблицу значений.
  2. Данные разбиваются на $N$ равных частей по выбранному критерию (например, по списку складов).
  3. Для каждого сегмента данных вызывается фоновое задание: ФоновыеЗадания.Выполнить("ИмяОбщегоМодуля.ЗаписатьПорциюДанных", ПараметрыПроцедуры).
  4. Каждое фоновое задание в цикле записывает свою порцию наборами записей с использованием транзакций по 10 000 строк.

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

---

Сравнительная таблица методов оптимизации

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

Метод записи Скорость работы Сложность реализации Безопасность для СУБД Когда применять
Построчно через МенеджерЗаписи Очень низкая (до 300-400 тыс. строк в час) Очень простая Высокая (нет блокировок, но грузит диск) Только для малых объемов (до 10 000 строк)
Пакетная запись порциями в транзакциях Высокая (до 5-10 млн строк в час) Средняя Средняя (требует контроля размера пакета) Основной стандартный способ для любых РС
Метод Загрузить() для всего набора Максимальная для одного потока Простая Низкая (блокирует таблицу, очищает старые данные) Когда нужно полностью перезаписать данные по отбору
РежимЗамещения.Слияние (1С 8.3.25+) Очень высокая Простая Высокая (оптимизировано на уровне ядра СУБД) При работе на современных версиях платформы
Параллельные фоновые задания Максимально возможная (ограничена только железом) Высокая Требует осторожности (возможны Deadlocks) Для регулярной загрузки сверхбольших объемов данных

---

Резюме и практические рекомендации

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

  1. Всегда оборачивайте запись пакетов данных в явные транзакции с помощью НачатьТранзакцию() и ЗафиксироватьТранзакцию(). Оптимальный размер пакета — 20 000 - 50 000 строк.
  2. Используйте объект РегистрСведенийНаборЗаписей вместо построчного обхода с объектом РегистрСведенийМенеджерЗаписи.
  3. Перед записью набора всегда устанавливайте свойство НаборЗаписей.ОбменДанными.Загрузка = Истина, чтобы отключить триггеры, проверки и авторегистрацию изменений на планах обмена.
  4. Для переноса полей из выборки запроса в строки набора применяйте быстрый системный метод ЗаполнитьЗначенияСвойств().
  5. Если вы работаете на платформе 1С версии 8.3.25 и выше, используйте современные системные режимы записи, такие как РежимЗамещения.Слияние или РежимЗамещения.Добавление.

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

← На главную