Как правильно удалить записи в регистре сведений 1С по значению ресурса?

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

При работе с регистрами сведений в платформе 1С:Предприятие разработчики часто сталкиваются с необходимостью удаления записей по критериям, отличным от ключевых полей. Стандартный механизм платформы позволяет легко устанавливать отборы в объекте НаборЗаписей только по измерениям регистра. Если же стоит задача удалить записи, ориентируясь на значение ресурса или реквизита, стандартный метод отбора вызвать не удастся — система выдаст ошибку «Поле объекта не обнаружено».

В этой статье мы подробно разберем, почему простые способы обхода коллекции «в лоб» приводят к ошибкам, как правильно организовать обход записей в оперативной памяти и какие профессиональные архитектурные подходы следует использовать для удаления данных из больших или подчиненных регистратору таблиц без угроз для производительности системы.

Почему простой цикл «Для Каждого» пропускает записи?

Проанализируем типичную ошибку начинающих разработчиков. Посмотрим на следующий код:


Для Каждого Запись из НаборЗаписей Цикл
    Если Запись.Ресурс = 5 Тогда
        НаборЗаписей.Удалить(Запись);
    КонецЕсли;
КонецЦикла;

На первый взгляд логика абсолютно верна. Однако при выполнении этого кода мы заметим, что часть записей, соответствующих условию, остается в регистре. Выясним причину этого странного поведения.

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

Способ 1. Обход коллекции с помощью цикла «Пока» и ручным сдвигом индекса

Рассмотрим наиболее лаконичный и правильный вариант решения проблемы прямо в оперативной памяти. Чтобы избежать пропуска записей при сдвиге индексов, мы можем управлять счетчиком цикла вручную. Увеличивать индекс мы будем только в том случае, если текущая запись НЕ была удалена:


Счетчик = 0;
Пока Счетчик < НаборЗаписей.Количество() Цикл
    ТекущаяСтрока = НаборЗаписей.Получить(Счетчик);
    Если ТекущаяСтрока.Ресурс = 5 Тогда
        НаборЗаписей.Удалить(ТекущаяСтрока);
        // Индекс не увеличиваем, так как следующая строка сместилась на место удаленной
    Иначе
        Счетчик = Счетчик + 1;
    КонецЕсли;
КонецЦикла;

Этот метод гарантирует, что ни одна запись не будет упущена, так как мы проверяем каждый элемент на текущей позиции индекса до тех пор, пока условие удаления не перестанет выполняться для этой позиции.

Способ 2. Обратный цикл (обход с конца коллекции)

Еще одно изящное решение проблемы сдвига индексов — обходить коллекцию с конца к началу. При удалении элемента в конце коллекции индексы еще не проверенных элементов (которые находятся ближе к началу) не изменяются. Разберем этот пример:


КоличествоЗаписей = НаборЗаписей.Количество();
Для Счетчик = 1 По КоличествоЗаписей Цикл
    Индекс = КоличествоЗаписей - Счетчик;
    Запись = НаборЗаписей[Индекс];
    Если Запись.Ресурс = 5 Тогда
        НаборЗаписей.Удалить(Запись);
    КонецЕсли;
КонецЦикла;

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

Способ 3. Использование промежуточного массива

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


МассивДляУдаления = Новый Массив;
Для Каждого Запись Из НаборЗаписей Цикл
    Если Запись.Ресурс = 5 Тогда
        МассивДляУдаления.Добавить(Запись);
    КонецЕсли;
КонецЦикла;

Для Каждого ЗаписьДляУдаления Из МассивДляУдаления Цикл
    НаборЗаписей.Удалить(ЗаписьДляУдаления);
EndDo;

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

Важное предупреждение: Все три описанных выше способа отлично подходят, когда набор записей уже прочитан в память и его размер относительно невелик. Но что делать, если в вашем регистре миллионы строк? Попытка прочитать весь регистр в один объект НаборЗаписей приведет к критической перегрузке оперативной памяти и зависанию всей системы.

Способ 4. Профессиональный метод для независимых регистров: «Запрос + Менеджер записи»

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

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

Рассмотрим по шагам схему реализации этого метода:


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

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Создаем менеджер записи для точечного удаления по ключу
    МенеджерЗаписи = РегистрыСведений.ИмяВашегоРегистра.СоздатьМенеджерЗаписи();
    ЗаполнитьЗначенияСвойств(МенеджерЗаписи, Выборка);
    
    // КРИТИЧЕСКИ ВАЖНО: Метод Прочитать() вызывать не нужно!
    // Достаточно просто заполнить ключевые измерения и вызвать Удалить().
    МенеджерЗаписи.Удалить(); 
КонецЦикла;

Этот подход экономит колоссальное количество ресурсов сервера, так как мы не передаем тяжелые наборы записей по сети и не забиваем оперативную память ненужными данными.

Способ 5. Специфика подчиненных регистров (зависимых от регистратора)

Если ваш регистр сведений имеет режим записи «Подчинение регистратору», использовать напрямую МенеджерЗаписи для удаления нельзя — платформа заблокирует эту операцию. В таких регистрах все движения жестко привязаны к документам-регистраторам.

Чтобы решить задачу для зависимого регистра, мы должны действовать по следующему алгоритму:

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

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	Р.Регистратор КАК Регистратор
    |ИЗ
    |	РегистрСведений.ИмяВашегоРегистра КАК Р
    |ГДЕ
    |	Р.Ресурс = &ЗначениеРесурса";
Запрос.УстановитьПараметр("ЗначениеРесурса", 5);

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

Способ 6. Порционное удаление для сверхбольших объемов данных (Батчинг)

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

В таких сценариях мы должны применять порционное удаление (батчинг). Мы будем удалять данные частями (например, по 1000 записей за транзакцию) и делать небольшие паузы для разгрузки процессора:


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

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

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

Резюме

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

← На главную