При активной эксплуатации баз данных 1С нередко возникает необходимость очистки устаревших данных в независимых регистрах сведений. Типичный пример — регистры цен поставщиков, курсов валют или истории изменений, куда непрерывно поступает информация. Со временем объем таблицы вырастает до миллионов записей, а бизнес-логика требует выборочного удаления устаревших строк (например, цен старше месяца, но только если по номенклатуре есть более свежая цена). Попытка удалить сотни тысяч записей стандартными методами часто приводит к эскалации блокировок СУБД, таймаутам и остановке параллельных фоновых заданий.
Разберем основные трудности, возникающие при массовом удалении, и рассмотрим эффективные программные способы решения этой задачи средствами платформы 1С:Предприятие.
Проанализируем технические ограничения и особенности платформы при работе с регистрами сведений:
РегистрСведенийНаборЗаписей позволяет очищать данные только при наличии жесткого отбора по ведущим измерениям или путем полной перезаписи всей таблицы с признаком Замещать = Истина. Если критерий удаления сложный (не укладывается в простой отбор по одному значению измерения), очистить данные одной командой набора невозможно.РегистрСведенийМенеджерЗаписи для построчного удаления генерирует отдельные запросы и транзакции на уровне СУБД для каждой строки, что крайне медленно при объемах в сотни тысяч строк.Наиболее надежный и безопасный платформенный способ — выборка ключей записей, подлежащих удалению, и их построчное удаление порциями (пачками) внутри коротких явных транзакций с отключением обработчиков.
Рассмотрим по шагам реализацию данного алгоритма:
РегистрСведенийМенеджерЗаписи.ОбменДанными.Загрузка = Истина, чтобы избежать срабатывания лишних проверок и регистрации изменений.Посмотрим на пример программного кода:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Цены.Период КАК Период,
| Цены.Номенклатура КАК Номенклатура,
| Цены.Поставщик КАК Поставщик
|ИЗ
| РегистрСведений.ЦеныПоставщиков КАК Цены
|ГДЕ
| Цены.Период < &ГраницаУдаления
| И ЕСТЬНЕNULL(Цены.Номенклатура.Ссылка, ИСТИНА)"; // Пример сложного условия
Запрос.УстановитьПараметр("ГраницаУдаления", ДобавитьМесяц(ТекущаяДатаСеанса(), -1));
Выборка = Запрос.Выполнить().Выбрать();
РазмерПорции = 1000;
Счетчик = 0;
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
МенеджерЗаписи = РегистрСведений.ЦеныПоставщиков.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = Выборка.Период;
МенеджерЗаписи.Номенклатура = Выборка.Номенклатура;
МенеджерЗаписи.Поставщик = Выборка.Поставщик;
// Отключаем бизнес-логику и подписки для ускорения
МенеджерЗаписи.ОбменДанными.Загрузка = Истина;
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
МенеджерЗаписи.Удалить();
Счетчик = Счетчик + 1;
КонецЕсли;
Если Счетчик % РазмерПорции = 0 Тогда
ЗафиксироватьТранзакцию();
// Небольшая пауза для разгрузки СУБД и пропуска параллельных транзакций
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
Если Счетчик % РазмерПорции <> 0 Тогда
ЗафиксироватьТранзакцию();
Иначе
ОтменитьТранзакцию();
КонецЕсли;
Если удаляемые данные можно сгруппировать по ключевому измерению (например, по контрагенту или группе товаров), гораздо быстрее работать через наборы записей, считывая и перезаписывая только данные конкретного разреза.
Разберем последовательность действий:
РегистрСведенийНаборЗаписей с установленным отбором.Прочитать() для загрузки записей только по этому объекту.Замещать = Истина и флагом ОбменДанными.Загрузка = Истина.Посмотрим на пример реализации:
// Получаем уникальных поставщиков, по которым требуется чистка
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Цены.Поставщик КАК Поставщик
|ИЗ
| РегистрСведений.ЦеныПоставщиков КАК Цены
|ГДЕ
| Цены.Период < &ГраницаУдаления";
Запрос.УстановитьПараметр("ГраницаУдаления", ГраницаУдаления);
ВыборкаПоставщиков = Запрос.Выполнить().Выбрать();
Пока ВыборкаПоставщиков.Следующий() Цикл
Набор = РегистрСведений.ЦеныПоставщиков.СоздатьНаборЗаписей();
Набор.Отбор.Поставщик.Установить(ВыборкаПоставщиков.Поставщик);
Набор.ОбменДанными.Загрузка = Истина;
Набор.Прочитать();
Индекс = Набор.Количество() - 1;
БылМодифицирован = Ложь;
Пока Индекс >= 0 Цикл
СтрокаНабора = Набор[Индекс];
Если СтрокаНабора.Период < ГраницаУдаления И Не ЭтоПоследняяЦена(СтрокаНабора) Тогда
Набор.Удалить(Индекс);
БылМодифицирован = Истина;
КонецЕсли;
Индекс = Индекс - 1;
КонецЦикла;
Если БылМодифицирован Тогда
Набор.Записать(Истина);
КонецЕсли;
КонецЦикла;
Если удаление записей требуется выполнять регулярно на работающей базе, процедуру следует обязательно выносить в фоновое или регламентное задание. Рекомендуется настроить расписание на ночное или наименее загруженное время суток. При выполнении в фоновом режиме порции можно уменьшить (например, до 200–500 записей), добавив паузу между транзакциями с помощью метода Приостановить() (или ожидания в несколько миллисекунд), что позволит другим пользователям и заданиям беспрепятственно работать с таблицей.
Если регистр постоянно переполняется и требует регулярной чистки, проанализируем возможные архитектурные доработки:
БлокировкаДанных строго на те значения измерений, которые обрабатываются в текущей итерации, чтобы предотвратить появление взаимоблокировок (deadlocks).