В процессе эксплуатации информационных систем на базе платформы 1С:Предприятие разработчики часто сталкиваются с проблемой лавинообразного роста объема баз данных. Одним из основных «потребителей» дискового пространства являются служебные регистры сведений, в которых накапливается история: логи обменов, истории изменения объектов, версионирование данных или протоколы выполнения регламентных заданий. Периодическая очистка таких регистров становится критически важной задачей для поддержания высокой производительности СУБД.
В этой статье мы подробно разберем, почему возникает популярная ошибка Недопустимый тип сравнения при попытке очистить периодический регистр по интервалу дат, а также рассмотрим несколько эффективных способов решения этой задачи — от простых скриптов на встроенном языке до прямого удаления на уровне систем управления базами данных (СУБД).
Рассмотрим классическую ошибку, с которой сталкиваются разработчики при первой попытке очистить историю. При попытке установить интервал дат в отборе набора записей регистра сведений возникает сбой:
НаборЗаписей = РегистрыСведений.ИсторияОбменаДанными.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Период.ВидСравнения = ВидСравнения.ИнтервалВключаяГраницы; // Ошибка!
Выясним причину такого поведения платформы. Объект РегистрСведенийНаборЗаписей спроектирован для работы с жестко ограниченными порциями данных. Согласно архитектуре 1С, коллекция Отбор для набора записей независимого регистра сведений поддерживает исключительно единственный вид сравнения — Равно. Платформа накладывает жесткое ограничение: использовать интервалы дат (такие как ИнтервалВключаяГраницы, Больше, Меньше), списки значений или отрицания в отборе набора записей напрямую нельзя.
Поэтому перед нами встает задача: как обойти это ограничение и эффективно удалить миллионы записей за определенный период, не перегружая при этом сервер и транзакционный лог.
Рассмотрим ситуацию, когда в регистре сведений за удаляемый период уникальных дат гораздо меньше, чем самих записей (то есть на одну и ту же секунду или день приходится множество записей). В этом случае мы можем выбрать все уникальные даты за целевой период с помощью запроса, а затем в цикле очистить данные по каждой дате.
Разберем по шагам правильную реализацию этого алгоритма. В первоначальных вариантах кода разработчики часто делают избыточные действия, такие как чтение набора, выгрузка его в таблицу значений, очистка таблицы и обратная загрузка. На самом деле для удаления данных достаточно просто записать пустой набор с установленным отбором. Проанализируем оптимизированный вариант:
// Определяем границы очистки данных
ДатаНачала = '00010101';
ДатаОкончания = ДобавитьМесяц(ТекущаяДата(), -24); // Оставляем данные только за последние 2 года
// Выбираем уникальные периоды (даты) из регистра
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| РегистрацияИзмененияВОбъектах.Дата КАК Дата
|ИЗ
| РегистрСведений.РегистрацияИзмененияВОбъектах КАК РегистрацияИзмененияВОбъектах
|ГДЕ
| РегистрацияИзмененияВОбъектах.Дата МЕЖДУ &ДатаНач И &ДатаКон
|УПОРЯДОЧИТЬ ПО
| Дата";
Запрос.УстановитьПараметр("ДатаНач", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаКон", КонецДня(ДатаОкончания));
Выборка = Запрос.Выполнить().Выбрать();
// Создаем пустой набор записей
НаборЗаписей = РегистрыСведений.РегистрацияИзмененияВОбъектах.СоздатьНаборЗаписей();
Пока Выборка.Следующий() Цикл
ОбработкаПрерыванияПользователя();
// Метод Установить() автоматически взводит Использование в Истина
НаборЗаписей.Отбор.Дата.Установить(Выборка.Дата);
// Записываем пустой набор. Это действие мгновенно удаляет все записи на эту дату
НаборЗаписей.Записать(Истина);
КонецЦикла;
Обратите внимание: метод Установить() автоматически включает свойство Использование = Истина для элемента отбора. Дополнительно писать строку включения отбора не требуется. Также мы полностью отказались от вызовов Прочитать() и Выгрузить(), что существенно сократило количество дисковых операций чтения и обращений к оперативной памяти.
Если объем данных колоссальный, то построчное удаление в цикле по-прежнему может выполняться медленно, так как каждая запись пустого набора инициирует отдельную транзакцию на уровне СУБД. Чтобы ускорить этот процесс в несколько раз, применим группировку операций в транзакции и отключение дополнительной бизнес-логики.
Проанализируем пример кода, оптимизированный для удаления больших объемов данных:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ИсторияОбменаДанными.Период КАК Период,
| ИсторияОбменаДанными.УзелИнформационнойБазы КАК УзелИнформационнойБазы
|ИЗ
| РегистрСведений.ИсторияОбменаДанными КАК ИсторияОбменаДанными
|ГДЕ
| ИсторияОбменаДанными.Период < &ПредельнаяДата";
Запрос.УстановитьПараметр("ПредельнаяДата", ДобавитьМесяц(ТекущаяДата(), -12));
ТаблицаПериодов = Запрос.Выполнить().Выгрузить();
НаборЗаписей = РегистрыСведений.ИсторияОбменаДанными.СоздатьНаборЗаписей();
// Отключаем выполнение обработчиков событий и регистрацию изменений
НаборЗаписей.ОбменДанными.Загрузка = Истина;
Счетчик = 0;
РазмерПорции = 1000; // Оптимальный размер порции для одной транзакции
НачатьТранзакцию();
Для Каждого СтрокаПериода Из ТаблицаПериодов Цикл
ОбработкаПрерыванияПользователя();
НаборЗаписей.Отбор.Период.Установить(СтрокаПериода.Период);
НаборЗаписей.Отбор.УзелИнформационнойБазы.Установить(СтрокаПериода.УзелИнформационнойБазы);
НаборЗаписей.Записать(Истина);
Счетчик = Счетчик + 1;
// Каждые 1000 записей фиксируем транзакцию и открываем новую
Если Счетчик % РазмерПорции = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию();
КонецЕсли;
Использование свойства ОбменДанными.Загрузка = Истина блокирует выполнение "тяжелого" кода в модулях регистра (события ПередЗаписью и ПриЗаписи), предотвращая зависание системы и экономя ресурсы процессора. Группировка по 1000 итераций в рамках одной транзакции снижает накладные расходы СУБД на блокировки таблиц и транзакционные логи.
Если перед вами стоит задача очистить огромный регистр за несколько лет, оставив при этом лишь небольшой срез данных (например, только за последний месяц), стандартный перебор дат будет неэффективен. Вместо удаления старых записей гораздо быстрее сохранить нужные данные во временную таблицу значений, полностью очистить регистр одним запросом, а затем записать сохраненные данные обратно.
Рассмотрим, как реализовать этот подход на практике:
// Шаг 1. Выбираем данные, которые необходимо ОСТАВИТЬ
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| *
|ИЗ
| РегистрСведений.ИсторияОбменаДанными КАК ИсторияОбменаДанными
|ГДЕ
| ИсторияОбменаДанными.Период >= &ДатаНачалаОставляемыхДанных";
Запрос.УстановитьПараметр("ДатаНачалаОставляемыхДанных", ДобавитьМесяц(ТекущаяДата(), -1));
ТаблицаОставляемыхДанных = Запрос.Выполнить().Выгрузить();
// Шаг 2. Полностью очищаем регистр сведений
НаборОчистки = РегистрыСведений.ИсторияОбменаДанными.СоздатьНаборЗаписей();
НаборОчистки.ОбменДанными.Загрузка = Истина;
// Запись пустого набора без установленных отборов мгновенно очищает весь регистр
НаборОчистки.Записать(Истина);
// Шаг 3. Записываем сохраненные полезные данные обратно
НаборВосстановления = РегистрыСведений.ИсторияОбменаДанными.СоздатьНаборЗаписей();
НаборВосстановления.ОбменДанными.Загрузка = Истина;
НаборВосстановления.Загрузить(ТаблицаОставляемыхДанных);
НаборВосстановления.Записать(Истина);
Важное предупреждение: Данный метод идеален только тогда, когда объем оставляемых данных относительно невелик. Если оставляемый объем исчисляется миллионами записей, попытка выгрузить их в оперативную память сервера приложений через метод Выгрузить() может вызвать критическую ошибку нехватки памяти (Out of Memory) и аварийно завершить рабочий процесс 1С.
Когда размер служебного регистра превышает десятки миллионов записей, штатные объектные методы платформы 1С могут удалять данные часами, парализуя работу пользователей из-за блокировок таблиц. В таких сценариях администраторы баз данных часто используют прямое удаление данных с помощью SQL-запросов.
Разберем по шагам схему безопасного удаления через SQL:
ПолучитьСтруктуруХраненияБазыДанных(). Нам нужно сопоставить имя регистра (например, "ИсторияОбменаДанными") с именем таблицы (например, _InfoReg12345) и колонками периода (например, _Period).
-- Пример скрипта для MS SQL Server
DECLARE @RowsDeleted INT
SET @RowsDeleted = 1
WHILE (@RowsDeleted > 0)
BEGIN
DELETE TOP (10000)
FROM _InfoReg12345
WHERE _Period < '2025-01-01 00:00:00'
SET @RowsDeleted = @@ROWCOUNT
-- Небольшая пауза для снятия блокировок и разгрузки дисковой подсистемы
WAITFOR DELAY '00:00:01'
END
В современных конфигурациях, созданных на базе Библиотеки стандартных подсистем (БСП), ручное написание кода для очистки служебных регистров часто избыточно. В БСП встроен готовый функционал для автоматического сокращения логов, очистки устаревших версий объектов и удаления ненужных временных файлов. Перед тем как писать собственный код, рекомендуем проверить настройки программы в разделе «Обслуживание» – «Очистка ненужных данных» или ознакомиться с программными интерфейсами общего модуля БСП.