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

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

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

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

В чем сложность стандартного удаления? Анализ проблемы

Рассмотрим типовую ситуацию, с которой часто сталкиваются разработчики. Нам необходимо обойти базу данных, найти определенные записи в подчиненном регистре по какому-то условию и удалить их. Типичный (но крайне неоптимальный) код выглядит следующим образом:


НаборЗаписей = РегистрыСведений.Хранилище.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
НаборЗаписей.Прочитать();

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

Для каждого ЭлтСз Из сзУдаляемыеЗаписи Цикл
    НаборЗаписей.Удалить(ЭлтСз.Значение);
КонецЦикла;

НаборЗаписей.Записать();

Почему этот код работает медленно?

  1. Множественные циклы: Сначала мы обходим набор записей для поиска удаляемых строк, затем обходим список значений для их удаления, а затем платформа выполняет запись набора в базу данных.
  2. Частые транзакции базы данных: На каждый документ-регистратор система инициирует отдельную транзакцию записи на диске. Если у нас тысячи регистраторов, это парализует работу СУБД.
  3. Вызов тяжелых обработчиков: При вызове метода Записать() платформа по умолчанию запускает все подписки на события и обработчики модуля набора записей (ПередЗаписью, ПриЗаписи), что создает колоссальную нагрузку.

Давайте разберем пошагово, как можно оптимизировать этот алгоритм и сделать его выполнение практически мгновенным.

Способ 1. Удаление записей с конца (обратный цикл)

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

Реализуем обход набора записей в обратном порядке:


НаборЗаписей = РегистрыСведений.Хранилище.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
НаборЗаписей.Прочитать();

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

НаборЗаписей.Записать();

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

Способ 2. Использование Таблицы Значений в качестве буфера

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

Рассмотрим этот вариант подробнее:


НаборЗаписей = РегистрыСведений.Хранилище.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
НаборЗаписей.Прочитать();

// Выгружаем данные в Таблицу Значений
ТаблицаДанных = НаборЗаписей.Выгрузить();

// Находим строки, которые подлежат удалению
ПараметрыОтбора = Новый Структура("ИмяКолонкиДляФильтра", ЗначениеДляУдаления);
НайденныеСтроки = ТаблицаДанных.НайтиСтроки(ПараметрыОтбора);

Для Каждого СтрокаДляУдаления Из НайденныеСтроки Цикл
    ТаблицаДанных.Удалить(СтрокаДляУдаления);
КонецЦикла;

// Загружаем очищенные данные обратно в набор и записываем
НаборЗаписей.Загрузить(ТаблицаДанных);
НаборЗаписей.Записать();

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

Способ 3. Запросный метод — самый быстрый способ для массового удаления

Когда документов-регистраторов сотни или тысячи, поочередное чтение и запись каждого набора в цикле будет занимать неприемлемо много времени. Лучшее архитектурное решение в данном случае — переложить задачу фильтрации на сторону СУБД с помощью запроса.

Идея метода заключается в следующем: мы пишем запрос, который выбирает из регистра все записи, кроме тех, которые мы хотим удалить. То есть мы сразу получаем готовые «чистые» наборы данных. Затем мы обходим этот результат, группируя его по регистраторам, и просто перезаписываем наборы полученными данными.

Проанализируем реализацию этого подхода:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    Хранилище.Регистратор КАК Регистратор,
    |    Хранилище.Измерение1 КАК Измерение1,
    |    Хранилище.Ресурс1 КАК Ресурс1
    |ИЗ
    |    РегистрСведений.Хранилище КАК Хранилище
    |ГДЕ
    |    НЕ (Хранилище.Измерение1 = &ИсключаемоеЗначение) // Условие «выживания» записей
    |
    |УПОРЯДОЧИТЬ ПО
    |    Регистратор
    |ИТОГИ ПО
    |    Регистратор";

Запрос.УстановитьПараметр("ИсключаемоеЗначение", ЗначениеКотороеУдаляем);

РезультатЗапроса = Запрос.Выполнить();
ВыборкаРегистратор = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

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

Преимущество: СУБД делает всю тяжелую работу по фильтрации строк. Мы не тратим время на чтение «лишних» записей в память платформы 1С, а сразу получаем готовые к записи наборы данных.

Критически важные оптимизации для крупных баз данных

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

1. Отключение бизнес-логики через ОбменДанными.Загрузка

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


НаборЗаписей.ОбменДанными.Загрузка = Истина;
НаборЗаписей.Записать();

2. Использование явных транзакций и пакетирования

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


КоличествоВТранзакции = 0;
НачатьТранзакцию();

Пока ВыборкаРегистратор.Следующий() Цикл
    // ... Код подготовки и записи набора ...
    НаборЗаписей.Записать();
    
    КоличествоВТранзакции = КоличествоВТранзакции + 1;
    Если КоличествоВТранзакции >= 500 Тогда
        ЗафиксироватьТранзакцию();
        КоличествоВТранзакции = 0;
        НачатьТранзакцию();
    КонецЕсли;
КонецЦикла;

Если ТранзакцияАктивна() Тогда
    ЗафиксироватьТранзакцию();
КонецЕсли;

3. Блокировка данных для исключения Deadlocks

В многопользовательской среде другие пользователи могут пытаться проводить документы, движения которых мы сейчас перезаписываем. Чтобы избежать взаимных блокировок (Deadlocks), перед началом чтения и модификации набора записей необходимо программно заблокировать этот диапазон данных:


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.Хранилище");
ЭлементБлокировки.УстановитьЗначение("Регистратор", ВыборкаРегистратор.Регистратор);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

Очистка записей с «битыми» ссылками (Объект не найден)

Часто проблема долгого удаления или некорректной работы регистров связана с тем, что сам документ-регистратор был физически удален из базы данных (например, прямой очисткой СУБД или некорректным обменом), а записи в регистре сведений остались («сироты» или «битые ссылки»).

Чтобы удалить такие записи, нет необходимости программно создавать или восстанавливать документы. Нам достаточно получить битую ссылку запросом (например, проверяя условие ГДЕ Регистратор.Номер ЕСТЬ NULL) и передать эту битую ссылку в отбор набора записей, после чего записать пустой набор:


// Передаем битую ссылку (Объект не найден) в отбор
НаборЗаписей = РегистрыСведений.Хранилище.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(БитаяСсылка);

// Записываем пустой набор — это физически удалит записи из базы данных
НаборЗаписей.Записать();

Важное замечание по платформе 8.3.25 и выше

В новых версиях платформы 1С:Предприятие появились сверхбыстрые режимы замещения при записи для независимых регистров сведений (такие как РежимЗамещения.Слияние и РежимЗамещения.Удаление). Обратите внимание, что эти механизмы не работают для подчиненных регистров сведений. При попытке применить их к регистрам, подчиненным регистратору, платформа сгенерирует исключение. Поэтому для подчиненных регистров классическая перезапись набора с отбором по регистратору остается единственным правильным и стандартным решением.

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

← На главную