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

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

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

Разберем основные трудности, возникающие при массовом удалении, и рассмотрим эффективные программные способы решения этой задачи средствами платформы 1С:Предприятие.

Почему возникают сложности при массовом удалении

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

  1. Ограничения набора записей: Объект РегистрСведенийНаборЗаписей позволяет очищать данные только при наличии жесткого отбора по ведущим измерениям или путем полной перезаписи всей таблицы с признаком Замещать = Истина. Если критерий удаления сложный (не укладывается в простой отбор по одному значению измерения), очистить данные одной командой набора невозможно.
  2. Накладные расходы менеджера записи: Использование РегистрСведенийМенеджерЗаписи для построчного удаления генерирует отдельные запросы и транзакции на уровне СУБД для каждой строки, что крайне медленно при объемах в сотни тысяч строк.
  3. Эскалация блокировок в СУБД: При попытке модифицировать большой массив данных в одной транзакции СУБД (MS SQL Server, PostgreSQL) автоматически переходит от строчных блокировок к блокировке страниц или всей таблицы, что полностью блокирует работу пользователей и фоновых процессов.
  4. Обработчики событий и подписки: При записи и удалении платформой по умолчанию вызываются обработчики модулей набора записей, подписки на события и механизмы регистрации изменений для планов обмена, что многократно увеличивает нагрузку на сервер.

Способ 1. Пакетное удаление порциями в коротких транзакциях

Наиболее надежный и безопасный платформенный способ — выборка ключей записей, подлежащих удалению, и их построчное удаление порциями (пачками) внутри коротких явных транзакций с отключением обработчиков.

Рассмотрим по шагам реализацию данного алгоритма:

  1. С помощью запроса получаем список записей для удаления (период и ключевые измерения регистра).
  2. Обходим результат запроса и для каждой строки инициализируем РегистрСведенийМенеджерЗаписи.
  3. Устанавливаем свойство ОбменДанными.Загрузка = Истина, чтобы избежать срабатывания лишних проверок и регистрации изменений.
  4. Группируем операции удаления в небольшие транзакции (например, по 500–1000 записей).

Посмотрим на пример программного кода:


Запрос = Новый Запрос;
Запрос.Текст = 
	"ВЫБРАТЬ
	|	Цены.Период КАК Период,
	|	Цены.Номенклатура КАК Номенклатура,
	|	Цены.Поставщик КАК Поставщик
	|ИЗ
	|	РегистрСведений.ЦеныПоставщиков КАК Цены
	|ГДЕ
	|	Цены.Период < &ГраницаУдаления
	|	И ЕСТЬНЕNULL(Цены.Номенклатура.Ссылка, ИСТИНА)"; // Пример сложного условия

Запрос.УстановитьПараметр("ГраницаУдаления", ДобавитьМесяц(ТекущаяДатаСеанса(), -1));
Выборка = Запрос.Выполнить().Выбрать();

РазмерПорции = 1000;
Счетчик = 0;

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

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

Если Счетчик % РазмерПорции <> 0 Тогда
	ЗафиксироватьТранзакцию();
Иначе
	ОтменитьТранзакцию();
КонецЕсли;

Способ 2. Очистка через наборы записей по ведущему измерению

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

Разберем последовательность действий:

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

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


// Получаем уникальных поставщиков, по которым требуется чистка
Запрос = Новый Запрос;
Запрос.Текст = 
	"ВЫБРАТЬ РАЗЛИЧНЫЕ
	|	Цены.Поставщик КАК Поставщик
	|ИЗ
	|	РегистрСведений.ЦеныПоставщиков КАК Цены
	|ГДЕ
	|	Цены.Период < &ГраницаУдаления";

Запрос.УстановитьПараметр("ГраницаУдаления", ГраницаУдаления);
ВыборкаПоставщиков = Запрос.Выполнить().Выбрать();

Пока ВыборкаПоставщиков.Следующий() Цикл
	
	Набор = РегистрСведений.ЦеныПоставщиков.СоздатьНаборЗаписей();
	Набор.Отбор.Поставщик.Установить(ВыборкаПоставщиков.Поставщик);
	Набор.ОбменДанными.Загрузка = Истина;
	Набор.Прочитать();
	
	Индекс = Набор.Количество() - 1;
	БылМодифицирован = Ложь;
	
	Пока Индекс >= 0 Цикл
		СтрокаНабора = Набор[Индекс];
		Если СтрокаНабора.Период < ГраницаУдаления И Не ЭтоПоследняяЦена(СтрокаНабора) Тогда
			Набор.Удалить(Индекс);
			БылМодифицирован = Истина;
		КонецЕсли;
		Индекс = Индекс - 1;
	КонецЦикла;
	
	Если БылМодифицирован Тогда
		Набор.Записать(Истина);
	КонецЕсли;
	
КонецЦикла;

Способ 3. Вынос операции в регламентное задание

Если удаление записей требуется выполнять регулярно на работающей базе, процедуру следует обязательно выносить в фоновое или регламентное задание. Рекомендуется настроить расписание на ночное или наименее загруженное время суток. При выполнении в фоновом режиме порции можно уменьшить (например, до 200–500 записей), добавив паузу между транзакциями с помощью метода Приостановить() (или ожидания в несколько миллисекунд), что позволит другим пользователям и заданиям беспрепятственно работать с таблицей.

Архитектурные рекомендации

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

  1. Разделение на текущие данные и архив: Создайте непериодический регистр для хранения только актуальных цен (минимальный объем, мгновенное чтение) и отдельный периодический регистр для истории цен. В таком случае удаление истории за прошлые периоды можно будет выполнять простым интервальным удалением без риска стереть последнюю действующую цену.
  2. Управляемые блокировки: При работе в режиме управляемых блокировок перед чтением и записью порции устанавливайте объект БлокировкаДанных строго на те значения измерений, которые обрабатываются в текущей итерации, чтобы предотвратить появление взаимоблокировок (deadlocks).
← На главную