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

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

Массовое удаление строк из регистра сведений — регулярная задача при очистке исторических данных, исправлении ошибок интеграции или оптимизации базы. Когда таблица содержит миллионы записей, использование объекта РегистрСведенийМенеджерЗаписи становится неэффективным: единичные операции вызывают колоссальные накладные расходы и длятся часами. Однако при попытке оптимизировать процедуру разработчики часто сталкиваются с неочевидными ограничениями стандартного набора записей.

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

Почему не работает отбор набора записей по массиву

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


НаборЗаписей = РегистрыСведений.ШтрихкодыНоменклатуры.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Штрихкод.Установить(МассивПорция500); // Ошибка
НаборЗаписей.Записать();

Выясним причину, по которой этот код не работает. Коллекция Отбор у объекта РегистрСведенийНаборЗаписей устроена иначе, чем отборы в динамических списках или отчетах. В элементе отбора набора записей поддерживается исключительно проверка на точное равенство единственному значению (вид сравнения Равно). Метод Установить() принимает конкретное значение, а не массив или список. Если передать массив, платформа воспримет его как попытку найти запись, где в поле записана ссылка на сам объект типа Массив, в результате чего отбор вернет пустой результат, либо платформа выбросит исключение несоответствия типов.

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

Решение с помощью механизма «РасширенныеРежимыЗамещения»

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

Для включения функционала используется свойство РасширенныеРежимыЗамещения, а сам тип операции задается системным перечислением РежимЗамещения. Чтобы удалить только требуемые записи без полной перезаписи регистра, воспользуемся значением РежимЗамещения.Удаление.

Разберем по шагам, как устроен этот алгоритм:

  1. Формируем запрос, выбирающий исключительно те строки, которые подлежат удалению из базы.
  2. Выгружаем результат запроса в таблицу значений. В ней обязательно должны присутствовать колонки всех измерений целевого регистра, так как СУБД сопоставляет удаляемые строки именно по ключевым полям.
  3. Создаем объект РегистрСведенийНаборЗаписей и активируем свойство РасширенныеРежимыЗамещения = Истина;.
  4. Загружаем в набор подготовленную таблицу и вызываем метод Записать(РежимЗамещения.Удаление).

Посмотрим на пример базовой реализации:


&НаСервере
Процедура УдалитьНенужныеШтрихкоды()

    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |   ШтрихкодыНоменклатуры.Штрихкод КАК Штрихкод,
    |   ШтрихкодыНоменклатуры.Номенклатура КАК Номенклатура,
    |   ШтрихкодыНоменклатуры.Характеристика КАК Характеристика,
    |   ШтрихкодыНоменклатуры.Упаковка КАК Упаковка
    |ИЗ
    |   РегистрСведений.ШтрихкодыНоменклатуры КАК ШтрихкодыНоменклатуры
    |ГДЕ
    |   НЕ ШтрихкодыНоменклатуры.УТР_Серия = ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)";
    
    ТЗ_Удаляемые = Запрос.Выполнить().Выгрузить();
    
    Если ТЗ_Удаляемые.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    НаборЗаписей = РегистрыСведений.ШтрихкодыНоменклатуры.СоздатьНаборЗаписей();
    НаборЗаписей.РасширенныеРежимыЗамещения = Истина;
    
    // Отключаем подписки и регистрацию изменений для максимальной скорости
    НаборЗаписей.ОбменДанными.Загрузка = Истина;
    
    НаборЗаписей.Загрузить(ТЗ_Удаляемые);
    НаборЗаписей.Записать(РежимЗамещения.Удаление);

КонецПроцедуры

На уровне СУБД этот механизм работает принципиально иначе, чем стандартная перезапись: строки загружаются во временную таблицу базы данных, после чего выполняется удаление через внутреннее соединение по измерениям регистра (DELETE ... FROM ... JOIN ...). Это исключает медленную построчную обработку и существенно экономит ресурсы сервера.

Оптимизация: пакетное удаление порциями

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

  1. Переполнение оперативной памяти: огромная таблица значений в памяти рабочего процесса rphost может вызвать дефицит RAM и аварийное завершение процессов сервера.
  2. Разрастание журнала транзакций: непрерывная транзакция на удаление сотен тысяч записей раздувает файл транзакционного лога СУБД (LDF в MS SQL Server или WAL в PostgreSQL), что грозит исчерпанием свободного места на диске.
  3. Эскалация блокировок: при длительной модификации большого объема строк СУБД накладывает монопольные блокировки на страницы и всю таблицу, парализуя работу пользователей с регистром.

Чтобы избежать этих проблем, выполним удаление циклически, разбивая поток на порции фиксированного размера (обычно от 5 000 до 50 000 записей за транзакцию). Обязательно убедимся, что по полю ресурса, участвующему в условии отбора, в конфигурации включено свойство Индексировать, иначе поиск каждой порции приведет к тяжелому полному сканированию таблицы регистра.

Рассмотрим надежный шаблон порционного удаления:


&НаСервере
Процедура УдалитьЗаписиПорциями()

    РазмерПачки = 10000;
    
    Пока Истина Цикл
    
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ " + Формат(РазмерПачки, "ЧГ=0") + "
        |   ШтрихкодыНоменклатуры.Штрихкод КАК Штрихкод,
        |   ШтрихкодыНоменклатуры.Номенклатура КАК Номенклатура,
        |   ШтрихкодыНоменклатуры.Характеристика КАК Характеристика,
        |   ШтрихкодыНоменклатуры.Упаковка КАК Упаковка
        |ИЗ
        |   РегистрСведений.ШтрихкодыНоменклатуры КАК ШтрихкодыНоменклатуры
        |ГДЕ
        |   НЕ ШтрихкодыНоменклатуры.УТР_Серия = ЗНАЧЕНИЕ(Справочник.СерииНоменклатуры.ПустаяСсылка)";
        
        ТЗ_Порция = Запрос.Выполнить().Выгрузить();
        
        Если ТЗ_Порция.Количество() = 0 Тогда
            Прервать; // Все записи, удовлетворяющие условию, удалены
        КонецЕсли;
        
        НачатьТранзакцию();
        Попытка
            НаборЗаписей = РегистрыСведений.ШтрихкодыНоменклатуры.СоздатьНаборЗаписей();
            НаборЗаписей.РасширенныеРежимыЗамещения = Истина;
            НаборЗаписей.ОбменДанными.Загрузка = Истина;
            
            НаборЗаписей.Загрузить(ТЗ_Порция);
            НаборЗаписей.Записать(РежимЗамещения.Удаление);
            
            ЗафиксироватьТранзакцию();
        Исключение
            ОтменитьТранзакцию();
            ВызватьИсключение;
        КонецПопытки;
        
    КонецЦикла;

КонецПроцедуры

Альтернативные варианты для версий платформы ниже 8.3.25

Если режим совместимости конфигурации не позволяет использовать РасширенныеРежимыЗамещения, можно применить классические инженерные подходы в зависимости от структуры регистра:

  1. Удаление наборами по ведущему измерению: если в регистре есть укрупненное ключевое поле (например, Номенклатура), запросом получают уникальный список этих объектов, для каждого в цикле устанавливают отбор НаборЗаписей.Отбор.Номенклатура.Установить(...), вызывают метод Прочитать(), программно удаляют из коллекции нужные строки и записывают набор обратно с флагом Замещение = Истина.
  2. Перенос полезных данных (при удалении большей части таблицы): если удалить нужно 90–95% записей, быстрее выгрузить запросом оставшиеся полезные строки во временную таблицу или служебный регистр, очистить исходный регистр целиком (через пустой набор записей без отборов), а затем вернуть сохраненные данные назад.
← На главную