Как ускорить удаление массива записей из большого регистра сведений, если известен только часть ключа?

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

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

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

Анализ проблемы: почему "лобовой" подход работает медленно?

Посмотрим на исходную ситуацию. У нас есть регистр с двумя измерениями, например, Организация и АкцизнаяМарка. Нам нужно удалить записи по списку акцизных марок. Первое измерение (Организация) нам неизвестно или не используется в отборе.

Часто программисты пишут следующий код:


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

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

В базе данных (SQL) записи физически упорядочены по кластерному индексу. Для регистра сведений кластерный индекс строится по измерениям в порядке их следования в конфигураторе (Измерение1 + Измерение2...).

Когда мы устанавливаем отбор только по второму измерению (АкцизнаяМарка), не указывая первое (Организация), SQL-сервер не может использовать кластерный индекс для быстрого поиска нужной строки. Ему приходится выполнять сканирование всей таблицы (Table Scan) или индекса (Index Scan), перебирая 50 миллионов записей, чтобы найти те, где Марка совпадает с нашей. Это происходит на каждой итерации цикла.

В обсуждении проблемы замеры показали, что удаление одной записи таким способом занимало около 45 секунд. Для 1000 записей это более 12 часов работы.

Правильное решение: использование полного ключа записи

Чтобы удаление происходило мгновенно, нам нужно заставить СУБД использовать поиск по кластерному индексу (Index Seek). Для этого мы должны указать значения всех измерений, входящих в ключ.

Так как мы знаем только одно измерение, нам нужно сначала выяснить значения остальных. Разобьем решение на два этапа:

  1. Запросом получаем полные ключи (значения всех измерений) для удаляемых записей.
  2. Удаляем записи точечно, указывая все измерения.

Этап 1: Получение данных запросом

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


// Подготовим массив значений для удаления
МассивМарок = Докум.АкцизныеМарки.ВыгрузитьКолонку("АкцизнаяМарка");

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	АкцизныеМаркиЕГАИС.ОрганизацияЕГАИС КАК ОрганизацияЕГАИС,
    |	АкцизныеМаркиЕГАИС.АкцизнаяМарка КАК АкцизнаяМарка
    |ИЗ
    |	РегистрСведений.АкцизныеМаркиЕГАИС КАК АкцизныеМаркиЕГАИС
    |ГДЕ
    |	АкцизныеМаркиЕГАИС.АкцизнаяМарка В(&АкцизнаяМарка)";

Запрос.УстановитьПараметр("АкцизнаяМарка", МассивМарок);
РезультатЗапроса = Запрос.Выполнить();

Этап 2: Точечное удаление записей

Теперь, когда у нас есть выборка с полными ключами (и Организация, и АкцизнаяМарка), мы можем удалять записи. Для этого можно использовать МенеджерЗаписи или НаборЗаписей с полным отбором.

Рассмотрим вариант с менеджером записи:


Выборка = РезультатЗапроса.Выбрать();

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

Такой подход сокращает время операции с 45 секунд до долей секунды (0.006 сек на запись). Ускорение составляет более чем в 170 раз.

Сравнение Менеджера записи и Набора записей

В процессе оптимизации может возникнуть вопрос: что работает быстрее — МенеджерЗаписи.Удалить() или НаборЗаписей.Записать()?

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

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


Выборка = РезультатЗапроса.Выбрать();

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

Тесты показывают, что разница между этими двумя методами находится на уровне статистической погрешности (0.0064 сек против 0.0066 сек). Главный фактор успеха — наличие полных данных для использования кластерного индекса СУБД.

Рекомендации по безопасности и производительности

При работе с такими операциями следует учитывать еще несколько важных моментов:

  1. Транзакции. Если вы удаляете 1000 записей в цикле по одной, каждое удаление — это отдельная транзакция СУБД. Это создает накладные расходы. Рекомендуется оборачивать цикл (или порции цикла по 100-500 элементов) в НачатьТранзакцию() / ЗафиксироватьТранзакцию(). Однако будьте осторожны: одна огромная транзакция на 1000 записей может вызвать блокировку таблицы.
  2. Блокировки. При массовом удалении даже с использованием индексов возможна эскалация блокировок (переход от блокировки строк к блокировке всей таблицы). Это остановит работу других пользователей. Поэтому лучше разбивать массив удаляемых данных на небольшие порции.
  3. Индексирование. Если получить первое измерение невозможно, можно включить свойство "Индексировать" у второго измерения в конфигураторе. Это создаст дополнительный индекс, который ускорит поиск, но увеличит размер базы данных и немного замедлит запись новых данных. Однако вариант с предварительным запросом (Этап 1) обычно предпочтительнее, так как не требует изменений структуры метаданных.

Итог

Если вам нужно удалить записи из огромного регистра, и вы знаете значение только одного измерения (не первого):

  1. Не пытайтесь удалять сразу, установив отбор только по этому полю.
  2. Сделайте запрос к регистру, чтобы получить значения всех измерений для целевых записей.
  3. Удаляйте записи, используя полный набор измерений в отборе (через Менеджер или Набор записей).
← На главную