Как правильно и быстро удалить записи из независимого регистра сведений в 1С 8.3?

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

При работе с базами данных 1С программисты регулярно сталкиваются с необходимостью очистки или выборочного удаления данных из независимых регистров сведений. Одной из классических задач является удаление записей, отобранных по определенному списку значений одного из измерений (например, удаление истории цен для определенного списка номенклатуры или очистка логов по списку пользователей). Для более масштабной очистки базы, когда требуется убрать сведения по конкретному юридическому лицу, применяется выборочное удаление организаций, автоматически очищающее связанные с ними объекты и регистры.

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

В чем сложность? Ограничения стандартного отбора

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

Первая идея, которая приходит в голову начинающему разработчику — установить в отборе набора записей массив значений, аналогично тому, как это делается в запросах (используя оператор В или В ИЕРАРХИИ). Однако платформа 1С накладывает жесткое ограничение: в объекте Отбор для наборов записей регистров сведений можно использовать только сравнение на равенство (аналог оператора = в SQL). Передать массив или список значений напрямую в отбор невозможно.

Из-за этого ограничения нам приходится использовать обходные пути. Рассмотрим их по шагам.

Способ №1. Классический перебор с использованием набора записей

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

Посмотрим на пример кода, реализующий этот подход:


НаборЗаписей = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей();

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

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

Почему нельзя использовать Менеджер Записи?

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

Объект МенеджерЗаписи предназначен для работы с одной конкретной строкой регистра. Чтобы удалить запись с его помощью, необходимо заполнить значения всех ключевых полей (измерений) регистра. Если у вашего регистра три измерения (например, Номенклатура, Склад, ТипЦен), а вы хотите удалить данные только по одной Номенклатуре (независимо от склада и типа цен), то МенеджерЗаписи использовать не получится. В таких случаях единственным инструментом остается НаборЗаписей.

Способ №2. Оптимизация удаления с помощью транзакций (пакетный метод)

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

Проанализируем пример кода, который разбивает процесс удаления на порции:


РазмерПорции = 1000; // Оптимальный размер порции для современных СУБД
ОбработаноЭлементов = 0;

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

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

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

Важное замечание по размеру порции: В базовых примерах часто указывают размер порции в 50 элементов. Однако на практике для современных СУБД (MS SQL Server, PostgreSQL) порция в 50 записей слишком мала. Оптимальным размером порции считается диапазон от 1000 до 5000 элементов. Более мелкие порции имеет смысл использовать только в высоконагруженных системах, чтобы избежать длительных блокировок таблиц и взаимных блокировок (deadlocks) при активной параллельной работе пользователей.

Способ №3. Использование режима загрузки данных (ОбменДанными.Загрузка)

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

Для ускорения процесса мы можем временно отключить выполнение этих обработчиков, установив свойство ОбменДанными.Загрузка в значение Истина. Посмотрим, как изменится наш код:


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

// Отключаем выполнение обработчиков событий и подписок
НаборЗаписей.ОбменДанными.Загрузка = Истина;

НаборЗаписей.Записать();

Этот простой шаг в сочетании с транзакциями дает колоссальный прирост производительности (в некоторых случаях скорость удаления возрастает в 5–10 раз).

Способ №4. «Обратный» метод (через сохранение нужных записей)

Если вам необходимо удалить значительную часть записей регистра (например, 80-90% от общего объема), то последовательное удаление даже порциями может оказаться неэффективным. В такой ситуации гораздо быстрее пойти от обратного:

  1. Выбрать с помощью запроса все записи регистра, которые НЕ подлежат удалению (то есть те, которые нам нужно оставить в базе).
  2. Выгрузить этот результат запроса в таблицу значений.
  3. Создать набор записей регистра вообще без установки отборов.
  4. Загрузить в него таблицу оставляемых записей методом НаборЗаписей.Загрузить() и вызвать метод Записать(Истина).

Проанализируем программную реализацию этого алгоритма:


// Шаг 1: Запрашиваем записи, которые нужно ОСТАВИТЬ в регистре
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	МойРегистр.Измерение1 КАК Измерение1,
    |	МойРегистр.Измерение2 КАК Измерение2,
    |	МойРегистр.Ресурс1 КАК Ресурс1
    |ИЗ
    |	РегистрСведений.МойРегистр КАК МойРегистр
    |ГДЕ
    |	НЕ МойРегистр.Измерение1 В (&МассивУдаляемыхЗначений)";

Запрос.УстановитьПараметр("МассивУдаляемыхЗначений", МассивНашихЗначений);
ТаблицаОставляемыхЗаписей = Запрос.Выполнить().Выгрузить();

// Шаг 2: Полностью перезаписываем регистр
НачатьТранзакцию();
Попытка
    НаборЗаписей = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей();
    
    // Отключаем обработчики для ускорения работы
    НаборЗаписей.ОбменДанными.Загрузка = Истина;
    
    // Загружаем только те записи, которые должны остаться
    НаборЗаписей.Загрузить(ТаблицаОставляемыхЗаписей);
    
    // Параметр Истина очищает весь регистр перед записью новых данных
    НаборЗаписей.Записать(Истина); 
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки;

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

Что делать на уровне СУБД после массового удаления?

При удалении сотен тысяч или миллионов записей из СУБД (будь то MS SQL Server или PostgreSQL) файлы базы данных физически не уменьшаются в размере, а в таблицах и индексах возникает сильная фрагментация. Это может привести к последующему замедлению работы запросов к данному регистру.

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

Подводя итог, отметим: для небольших списков удаления (до 100-500 элементов) прекрасно подойдет Способ №2 с порциями около 1000 элементов и обязательной установкой флага ОбменДанными.Загрузка = Истина. Для глобальной чистки регистров всегда рассматривайте Способ №4, как самый производительный инструмент в арсенале разработчика 1С.

← На главную