При работе с крупными базами данных в системе «1С:Предприятие» разработчики часто сталкиваются с задачами обработки больших массивов информации. Запись миллионов строк в регистры сведений стандартными методами может занимать часы, а иногда и приводить к зависанию сервера или переполнению памяти. В этой статье мы подробно разберем, как оптимизировать этот процесс, почему классический построчный обход выборки работает медленно и какие архитектурные подходы позволяют ускорить запись в десятки и сотни раз.
Давайте проанализируем классический код, с которого часто начинают разработчики (и который использовал автор темы в своем первом сообщении). В нем выполняется обход выборки запроса в цикле, и для каждой строки создается менеджер записи регистра, заполняются поля, после чего вызывается метод Записать():
Пока Выборка.Следующий() Цикл
НаборЗапись = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьМенеджерЗаписи();
НаборЗапись.Период = Выборка.Period;
// ... заполнение остальных реквизитов и измерений ...
НаборЗапись.Записать(Истина);
КонецЦикла;
Этот подход содержит в себе сразу несколько критических проблем производительности:
РегистрСведенийМенеджерЗаписи платформа «1С:Предприятие» выполняет под капотом три операции. Сначала она делает запрос к СУБД (SELECT), чтобы проверить, существует ли уже запись с такими ключевыми полями. Если запись существует, платформа выполняет ее удаление (DELETE), а затем вставляет новую строку (INSERT). Если записи нет, происходит только вставка. На объеме в 6 миллионов строк это превращается в 18 миллионов тяжелых операций с базой данных!Записать() без явно открытой транзакции инициирует отдельную транзакцию на уровне СУБД. База данных вынуждена открывать транзакцию, записывать одну-единственную строку, фиксировать транзакцию и сбрасывать буферы на диск. Дисковая подсистема сервера при этом испытывает колоссальную нагрузку (очередь к диску растет, производительность падает до критического минимума).СоздатьМенеджерЗаписи() вызывается на каждой итерации цикла. Платформа тратит процессорное время и оперативную память на выделение ресурсов под миллионы временных объектов.Для решения этой проблемы необходимо кардинально изменить подход к работе с транзакциями и использовать специализированные объекты для массовой работы с данными — наборы записей.
---
Самый простой и эффективный способ ускорить запись в цикле без изменения структуры метаданных — это объединение записей в группы (пакеты) и выполнение записи внутри транзакции. Давайте выясним, почему это работает.
Когда мы открываем транзакцию с помощью оператора НачатьТранзакцию(), СУБД выполняет запись данных в оперативную память (буферы) и в журнал транзакций во временном режиме. Физическая фиксация изменений на диске происходит только один раз — при вызове команды ЗафиксироватьТранзакцию(). Это позволяет сократить количество дисковых операций ввода-вывода (I/O) в тысячи раз.
Однако нельзя помещать все 6 миллионов строк в одну транзакцию. Слишком большая транзакция приведет к переполнению журнала транзакций СУБД, нехватке оперативной памяти на сервере приложений 1С и, что самое опасное, к блокировке всей таблицы регистра для других пользователей. Оптимальный размер пакета (порции) подбирается экспериментально и обычно составляет от 10 000 до 50 000 строк.
Рассмотрим пример реализации порционной записи в транзакциях с использованием объекта РегистрСведенийНаборЗаписей:
// Определяем размер пакета для записи
РазмерПакетнойЗаписи = 50000;
Выборка = Запрос.Выполнить().Выбрать();
СчетчикСтрок = 0;
// Создаем один набор записей, который будем использовать повторно
НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
// Добавляем новую запись в текущий набор
НоваяЗапись = НаборЗаписей.Добавить();
ЗаполнитьЗначенияСвойств(НоваяЗапись, Выборка);
НоваяЗапись.Пользователь = Пользователи.ТекущийПользователь();
СчетчикСтрок = СчетчикСтрок + 1;
// Если размер пакета достигнут, производим запись и фиксируем транзакцию
Если СчетчикСтрок % РазмерПакетнойЗаписи = 0 Тогда
// Записываем набор без замещения существующих данных (параметр Ложь)
НаборЗаписей.Записать(Ложь);
ЗафиксироватьТранзакцию();
// Очищаем набор записей для подготовки к следующему пакету
НаборЗаписей.Очистить();
// Начинаем новую транзакцию для следующей порции данных
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
// Не забываем записать остаток данных, который не вошел в последний полный пакет
Если НаборЗаписей.Количество() > 0 Тогда
НаборЗаписей.Записать(Ложь);
КонецЕсли;
ЗафиксироватьТранзакцию();
Обратите внимание на использование метода ЗаполнитьЗначенияСвойств(). Вместо ручного сопоставления десятков полей (как это было в исходном коде автора темы) мы используем системную функцию, которая работает на уровне платформы значительно быстрее. Также критически важно передавать параметр Ложь в метод Записать(). Это указывает платформе, что мы просто добавляем новые строки в таблицу, не пытаясь перезаписать или очистить существующие данные по отбору.
---
Запись миллионов строк может сильно замедляться из-за наличия обработчиков событий в модуле регистра сведений (таких как ПередЗаписью или ПриЗаписи) или подписок на события. В этих обработчиках могут выполняться тяжелые проверки, расчеты или запись в другие таблицы.
Для массовой технической заливки данных эту бизнес-логику необходимо временно отключить. В платформе 1С для этого предусмотрен стандартный механизм — свойство ОбменДанными.Загрузка. Давайте посмотрим, как правильно применить его при массовой записи наборов:
НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
// Установка признака загрузки данных
НаборЗаписей.ОбменДанными.Загрузка = Истина;
При установке этого свойства в значение Истина платформа автоматически отключает регистрацию изменений в планах обмена (что крайне важно, так как регистрация 6 миллионов изменений в планах обмена неизбежно приведет к зависанию базы данных). Кроме того, в самих обработчиках модуля регистра необходимо добавить проверку, чтобы избежать выполнения тяжелого кода:
Процедура ПередЗаписи(Отказ, Замещение)
Если ОбменДанными.Загрузка Тогда
Возврат; // Прерываем выполнение обработчика для ускорения записи
КонецЕсли;
// Обычная бизнес-логика...
КонецПроцедуры
---
Если результат вашего запроса можно сразу выгрузить в таблицу значений, вы можете использовать метод Загрузить() объекта РегистрСведенийНаборЗаписей. Этот метод работает на уровне платформы чрезвычайно быстро, так как перенос данных из структуры ТаблицаЗначений в структуру набора записей оптимизирован на уровне C++ ядра платформы.
При использовании этого метода важно помнить, что имена колонок в таблице значений должны полностью совпадать с именами измерений, ресурсов и реквизитов регистра сведений. Рассмотрим этот метод на примере:
// Выгружаем результат запроса напрямую в Таблицу Значений
ТаблицаРезультата = Запрос.Выполнить().Выгрузить();
// Добавляем колонку "Пользователь", если ее не было в запросе
ТаблицаРезультата.Колонки.Добавить("Пользователь");
ТаблицаРезультата.ЗаполнитьЗначения(Пользователи.ТекущийПользователь(), "Пользователь");
// Создаем набор записей
НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
НаборЗаписей.ОбменДанными.Загрузка = Истина;
// Загружаем всю таблицу значений в набор
НаборЗаписей.Загрузить(ТаблицаРезультата);
// Записываем данные в базу (Внимание: этот метод подходит, если набор перезаписывает данные по отбору)
НаборЗаписей.Записать(Истина);
Важное предупреждение: Метод Загрузить() полностью очищает старые записи в БД, соответствующие установленным отборам набора записей, перед вставкой новых. Если вы записываете данные без установки отборов (по всему регистру), СУБД попытается очистить весь регистр. Поэтому используйте этот метод с осторожностью и только тогда, когда вам действительно нужно перезаписать определенный сегмент данных (например, по конкретному периоду или объекту).
---
В современных версиях платформы «1С:Предприятие» (начиная с 8.3.25) появился революционный инструмент для массовой работы с базами данных — расширенные режимы записи наборов с использованием системного перечисления РежимЗамещения. Ранее у метода Записать() был только булев параметр (Истина/Ложь), что сильно ограничивало возможности оптимизации операций на уровне СУБД.
Теперь мы можем передавать в метод Записать() специальные режимы, которые СУБД выполняет нативно и максимально быстро:
РежимЗамещения.Добавление: Аналог быстрой вставки. Строки просто добавляются в конец таблицы без предварительных проверок. Идеально для пустых таблиц или гарантированно уникальных данных.РежимЗамещения.Слияние: Самый долгожданный режим (на уровне SQL это операция MERGE / UPSERT). Если строка с такими измерениями уже существует, СУБД обновляет ее ресурсы и реквизиты. Если строки нет — она просто добавляется. Это полностью решает проблему построчных проверок существования записей силами платформы!РежимЗамещения.Обновление: Производит только обновление существующих записей в базе данных. Если записи с такими измерениями нет, вставка не происходит. Это экономит ресурсы СУБД, так как операция UPDATE выполняется быстрее, чем INSERT.Посмотрим, как выглядит современный код записи с использованием режима слияния:
НаборЗаписей = РегистрыСведений.СтрокиНаОтправкуВСУМА.СоздатьНаборЗаписей();
НаборЗаписей.ОбменДанными.Загрузка = Истина;
// Заполняем набор записей порцией данных...
// ...
// Записываем в режиме Слияния (доступно в 1С 8.3.25+)
НаборЗаписей.Записать(РежимЗамещения.Слияние);
---
Если у сервера приложений 1С и сервера СУБД есть свободные вычислительные мощности (многоядерные процессоры, быстрые NVMe-накопители), запись 6 миллионов строк можно распараллелить. Вместо записи в один поток мы можем запустить, например, 8 или 16 параллельных фоновых заданий, каждое из которых будет записывать свою часть данных. Для реализации и контроля выполнения длительных процессов без БСП можно использовать пример работы с фоновыми заданиями.
Для успешного распараллеливания без возникновения взаимоблокировок (Deadlocks) необходимо строго соблюдать три правила:
БлокировкаДанных. Это предотвратит эскалацию блокировок СУБД до уровня всей таблицы регистра.Схематичный алгоритм параллельной записи выглядит следующим образом:
ФоновыеЗадания.Выполнить("ИмяОбщегоМодуля.ЗаписатьПорциюДанных", ПараметрыПроцедуры).Этот метод позволяет сократить общее время выполнения задачи практически пропорционально количеству выделенных потоков.
---
Давайте сопоставим рассмотренные методы по ключевым параметрам, чтобы вам было проще выбрать стратегию для вашей задачи:
| Метод записи | Скорость работы | Сложность реализации | Безопасность для СУБД | Когда применять |
|---|---|---|---|---|
| Построчно через МенеджерЗаписи | Очень низкая (до 300-400 тыс. строк в час) | Очень простая | Высокая (нет блокировок, но грузит диск) | Только для малых объемов (до 10 000 строк) |
| Пакетная запись порциями в транзакциях | Высокая (до 5-10 млн строк в час) | Средняя | Средняя (требует контроля размера пакета) | Основной стандартный способ для любых РС |
| Метод Загрузить() для всего набора | Максимальная для одного потока | Простая | Низкая (блокирует таблицу, очищает старые данные) | Когда нужно полностью перезаписать данные по отбору |
| РежимЗамещения.Слияние (1С 8.3.25+) | Очень высокая | Простая | Высокая (оптимизировано на уровне ядра СУБД) | При работе на современных версиях платформы |
| Параллельные фоновые задания | Максимально возможная (ограничена только железом) | Высокая | Требует осторожности (возможны Deadlocks) | Для регулярной загрузки сверхбольших объемов данных |
---
Для быстрой и безопасной записи 6 миллионов строк в регистр сведений воспользуйтесь следующим проверенным чек-листом:
НачатьТранзакцию() и ЗафиксироватьТранзакцию(). Оптимальный размер пакета — 20 000 - 50 000 строк.РегистрСведенийНаборЗаписей вместо построчного обхода с объектом РегистрСведенийМенеджерЗаписи.НаборЗаписей.ОбменДанными.Загрузка = Истина, чтобы отключить триггеры, проверки и авторегистрацию изменений на планах обмена.ЗаполнитьЗначенияСвойств().РежимЗамещения.Слияние или РежимЗамещения.Добавление.Применение этих простых правил позволит вам сократить время выполнения обработки с нескольких часов до нескольких минут, обеспечив при этом стабильную и плавную работу всей информационной системы.