В процессе разработки и оптимизации систем на платформе 1С:Предприятие 8 мы часто сталкиваемся с задачами обновления данных в крупных регистрах сведений. Типичный сценарий: существует регистр сведений, связанный с документами, где на один документ может приходиться 50 000 и более строк. В определенный момент возникает необходимость обновить значения ресурсов или реквизитов лишь для небольшой части этих строк — например, для 10, 100 или 1000 записей.
Стандартный подход с использованием объекта НаборЗаписей при перезаписи малого количества строк из большого объема показывает крайне низкую производительность. В этой статье мы подробно разберем, почему классические методы работают медленно, как решить эту проблему с помощью МенеджераЗаписи в транзакции, а также рассмотрим революционные механизмы точечного обновления данных, появившиеся в современных версиях платформы 1С.
Давайте проанализируем, как работает стандартный НаборЗаписей. Когда мы устанавливаем отбор по документу-регистратору (или ключевому измерению) и записываем набор в режиме замещения, платформа выполняет в СУБД две ресурсоемкие операции:
DELETE).INSERT).Если нам нужно изменить всего 100 строк из 50 000, то при использовании набора записей нам придется сначала прочитать все 50 000 строк в память сервера приложений, изменить нужные 100 строк в цикле, а затем перезаписать весь объем обратно в базу данных. В итоге СУБД удалит 50 000 записей и вставит новые 50 000 записей. Ресурсы сервера на обработку 49 900 не изменившихся строк будут потрачены впустую, что приведет к колоссальным задержкам и блокировкам таблиц.
Попытка записывать данные построчно через НаборЗаписей с фильтрацией по уникальному идентификатору строки в цикле также не приносит успеха: на каждую строку платформа будет инициировать отдельные транзакции СУБД, что порождает огромные накладные расходы на сетевое взаимодействие между сервером 1С и сервером СУБД.
Как показывает практика и реальные тесты разработчиков, для точечной модификации небольшого количества строк (до нескольких тысяч) гораздо эффективнее использовать объект МенеджерЗаписи. Вопреки старому мифу о том, что менеджер записи медленнее набора, при точечной работе он выигрывает в десятки раз. Однако здесь есть критически важное условие — вся выборка должна обрабатываться внутри единой транзакции.
Разберем по шагам правильный алгоритм реализации этого метода:
НачатьТранзакцию();
Попытка
Для Каждого ИдСтроки Из МассивСтрокДляМодификации Цикл
// Создаем менеджер записи для точечного обращения
МенеджерЗаписи = РегистрыСведений.ИмяВашегоРегистра.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Документ = СсылкаНаДокумент;
МенеджерЗаписи.ИдентификаторСтроки = ИдСтроки;
// Читаем конкретную строку из базы данных
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
// Модифицируем только необходимые реквизиты или ресурсы
МенеджерЗаписи.ИмяРеквизита = НовоеЗначениеРеквизита;
// Записываем измененную строку обратно в БД
МенеджерЗаписи.Записать();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Почему этот код работает быстрее? Во-первых, мы избавляемся от необходимости пересылать 50 000 строк по сети между СУБД и сервером 1С. Во-вторых, открытая нами явная транзакция НачатьТранзакцию() объединяет все дисковые операции записи (запись в файлы базы данных и логи транзакций) в один общий пул. Если убрать транзакцию, СУБД будет вынуждена физически фиксировать изменения на диске для каждой отдельной строки, что замедлит работу алгоритма в разы.
В реальных задачах количество изменяемых строк может динамически варьироваться. Если нам нужно изменить 10 строк из 50 000 — однозначно лидирует МенеджерЗаписи. Но если нужно обновить все 50 000 строк, то поштучный обход через менеджер записи будет выполняться бесконечно долго, и здесь гораздо выгоднее использовать один общий НаборЗаписей.
Мы рекомендуем спроектировать адаптивный алгоритм, который сам принимает решение о способе модификации данных в зависимости от объема изменений. Рассмотрим пример такого кода:
// Определяем пороговое значение (подбирается экспериментально, обычно от 500 до 2000 строк)
ПороговоеЗначение = 1000;
КоличествоИзменяемыхСтрок = МассивСтрокДляМодификации.Количество();
Если КоличествоИзменяемыхСтрок < ПороговоеЗначение Тогда
// Мало строк: используем быстрый точечный МенеджерЗаписи в транзакции
НачатьТранзакцию();
Попытка
Для Каждого ИдСтроки Из МассивСтрокДляМодификации Цикл
Менеджер = РегистрыСведений.ИмяВашегоРегистра.СоздатьМенеджерЗаписи();
Менеджер.Документ = СсылкаНаДокумент;
Менеджер.ИдентификаторСтроки = ИдСтроки;
Менеджер.Прочитать();
Если Менеджер.Выбран() Тогда
Менеджер.ВашРеквизит = НовоеЗначение;
Менеджер.Записать();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Иначе
// Много строк: перезаписываем весь набор за один раз
НаборЗаписей = РегистрыСведений.ИмяВашегоРегистра.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Документ.Установить(СсылкаНаДокумент);
НаборЗаписей.Прочитать();
Для Каждого Запись Из НаборЗаписей Цикл
// Ищем, входит ли строка в список изменяемых
Если МассивСтрокДляМодификации.Найти(Запись.ИдентификаторСтроки) <> Неопределено Тогда
Запись.ВашРеквизит = НовоеЗначение;
КонецЕсли;
КонецЦикла;
НаборЗаписей.Записать();
КонецЕсли;
В современных версиях платформы «1С:Предприятие 8» (начиная с 8.3.25) разработчики получили долгожданный нативный инструмент для решения этой проблемы. Теперь метод Записать() у объекта НаборЗаписей умеет принимать специальное системное перечисление РежимЗамещения вместо простого булева значения.
Появились новые режимы, которые кардинально меняют логику работы с СУБД:
РежимЗамещения.Обновление (Update): обновляет только те записи в СУБД, ключевые поля которых совпадают с переданным набором. Новые строки в базу не добавляются, а старые не затираются.РежимЗамещения.Слияние (Merge / Upsert): точечно обновляет существующие записи, совпавшие по ключевым полям, и добавляет в СУБД новые строки, которых ранее не было.Благодаря этому нововведению нам больше вообще не нужно предварительно читать все 50 000 записей регистра. Мы можем сформировать набор, содержащий исключительно 100 измененных строк, и выполнить быстрое обновление в СУБД одной командой:
// Создаем набор без установки отборов по измерениям на сам набор!
НаборЗаписей = РегистрыСведений.ИмяВашегоРегистра.СоздатьНаборЗаписей();
// Заполняем набор только теми строками, которые нужно обновить
Для Каждого СтрокаДанных Из ТаблицаНовыхДанных Цикл
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Документ = СсылкаНаДокумент;
НоваяЗапись.ИдентификаторСтроки = СтрокаДанных.ИдентификаторСтроки;
НоваяЗапись.ВашРеквизит = СтрокаДанных.НовоеЗначение;
КонецЦикла;
// Выполняем точечный UPDATE в СУБД только для переданных строк
НаборЗаписей.Записать(РежимЗамещения.Обновление);
Этот метод объединяет в себе лучшие стороны: простоту групповой работы через набор и колоссальную производительность точечных запросов к СУБД.
Чтобы гарантировать максимальную скорость выполнения операций и минимизировать вероятность взаимных блокировок в высоконагруженных базах данных, придерживайтесь следующих стандартов разработки:
БлокировкаДанных по ключевому измерению (например, по документу-владельцу). Это предотвратит эскалацию блокировок таблиц на уровне СУБД.ОбменДанными.Загрузка = Истина. Это заблокирует запуск подписок на события и тяжелых алгоритмов в модуле набора записей, что ускорит запись в несколько раз.