В процессе разработки на платформе 1С:Предприятие часто возникает задача очистки регистров сведений от устаревших данных. Если регистр является периодическим, мы можем легко отобрать записи по периоду. Однако, если регистр непериодический и независимый, а дата, по которой нужно произвести очистку, хранится не в измерениях, а в обычном реквизите, стандартный механизм Отбор в объекте РегистрСведенийНаборЗаписей не сработает. Платформа позволяет устанавливать отбор только по измерениям регистра.
В данной статье мы разберем, как обойти это ограничение, рассмотрим программные способы удаления таких записей (включая сценарии, когда требуется очистка записей регистров по битым документам) и проанализируем методы оптимизации для работы с большими объемами данных — для этого есть диагностика и удаление битых ссылок в регистрах.
Если вам не хочется писать код, можно использовать инструменты для универсальной очистки базы — есть универсальная обработка поиска и удаления лишних данных.
Самый простой и очевидный способ, подходящий для небольших и средних объемов данных, — это универсальная работа с регистрами (удобно через редактор регистров с массовым удалением записей) или получение уникальных ключей (всех измерений) записей, которые соответствуют нашему условию, с последующим удалением каждой записи через РегистрСведенийМенеджерЗаписи.
Проанализируем ситуацию: нам нужно удалить записи, где реквизит ДатаЗаписи меньше определенного значения. Также может потребоваться групповая корректировка записей регистров (поможет безопасное удаление битых ссылок в объектах базы). Разберем по шагам, как это реализовать:
Рассмотрим пример кода:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МойРегистр.Измерение1,
| МойРегистр.Измерение2,
| МойРегистр.Измерение3,
| МойРегистр.Измерение4,
| МойРегистр.Измерение5
|ИЗ
| РегистрСведений.МойРегистр КАК МойРегистр
|ГДЕ
| МойРегистр.ДатаЗаписи < &ГраницаДаты";
Запрос.УстановитьПараметр("ГраницаДаты", Объект.ДатаОчистки);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
МенеджерЗаписи = РегистрыСведений.МойРегистр.СоздатьМенеджерЗаписи();
// Заполняем все измерения из выборки
ЗаполнитьЗначенияСвойств(МенеджерЗаписи, Выборка);
// Пытаемся прочитать и удалить
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
МенеджерЗаписи.Удалить();
КонецЕсли;
КонецЦикла;
Важный момент: использование метода Прочитать() перед Удалить() гарантирует, что мы не попытаемся удалить уже несуществующую запись, что может случиться в многопользовательской среде.
Если записей для удаления тысячи или десятки тысяч, использование МенеджерЗаписи внутри цикла будет работать медленно, так как каждый вызов Удалить() инициирует отдельную транзакцию в базе данных (если цикл не обернут в общую транзакцию). Более эффективным методом является использование объекта РегистрСведенийНаборЗаписей.
Хотя мы не можем установить отбор по реквизиту в наборе, мы можем устанавливать отборы по измерениям для каждой конкретной комбинации, найденной запросом. Выясним причину ускорения: запись пустого набора с установленным отбором по измерениям работает быстрее, чем поиск и удаление объекта через менеджер.
Рассмотрим алгоритм пакетного удаления:
Выборка = Запрос.Выполнить().Выбрать();
НаборЗаписей = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей();
Пока Выборка.Следующий() Цикл
// Устанавливаем отборы по всем измерениям
НаборЗаписей.Отбор.Измерение1.Установить(Выборка.Измерение1);
НаборЗаписей.Отбор.Измерение2.Установить(Выборка.Измерение2);
// ... и так для всех 5 измерений
// Записываем пустой набор, что фактически удаляет запись
НаборЗаписей.Записать(Истина);
КонецЦикла;
Чтобы наш запрос в регламентном задании не «вешал» систему при поиске записей по реквизиту ДатаЗаписи, проанализируем настройки метаданных. Если в регистре миллионы записей, поиск по неиндексированному полю приведет к полному сканированию таблицы (Full Table Scan).
Рассмотрим рекомендации по настройке:
РегистрСведений.ДатаЗаписи) и откройте его свойства.Это позволит СУБД мгновенно находить записи по условию < Дата, что критически важно для производительности регламентных заданий.
При выполнении массового удаления в работающей базе данных мы неизбежно столкнемся с проблемой блокировок. Если мы попытаемся удалить 100 000 записей в одной транзакции, другие пользователи не смогут записывать данные в этот регистр до завершения операции.
Посмотрим, как правильно организовать удаление в регламентном задании:
ВЫБРАТЬ ПЕРВЫЕ 1000 в запросе. Выполняем удаление этой тысячи, завершаем транзакцию, делаем небольшую паузу (Ждать) и повторяем процесс, пока запрос не вернет пустой результат.БлокировкаДанных.Пример структуры кода для регламентного задания:
Пока Истина Цикл
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1000 Измерения... ГДЕ Дата < &Дата";
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда Прервать; КонецЕсли;
Выборка = Результат.Выбрать();
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
// Здесь логика удаления из Метода 1 или 2
КонецЦикла;
ЗафиксироватьТранзакцию();
// Пауза 1-2 секунды, чтобы дать поработать другим процессам
#Если Сервер Тогда
// Код для задержки
#КонецЕсли
КонецЦикла;
Если задача удаления старых записей по дате возникает регулярно, стоит проанализировать целесообразность изменения структуры регистра. Если реквизит ДатаЗаписи по смыслу является временем возникновения события, возможно, регистр стоит сделать периодическим. В этом случае поле Период станет штатным измерением, и вы сможете использовать стандартный метод НаборЗаписей.Отбор.Период.Установить() для удаления целых интервалов времени одной командой, что в десятки раз быстрее обработки каждой записи в цикле.
Важно: Не добавляйте лишние измерения (как предлагалось в некоторых сообщениях на форуме) только ради удобства удаления. Каждое новое измерение увеличивает размер индекса и замедляет запись новых данных. Используйте комбинацию индексированного реквизита и пакетного удаления через запрос — это наиболее сбалансированное решение.