Рассмотрим типовую, но сложную задачу оптимизации производительности при работе с большими объемами данных. Представим ситуацию: у нас есть независимый регистр сведений, в котором хранится более 50 миллионов записей. Нам необходимо удалить небольшой объем данных (например, 1000 записей), основываясь на списке значений одного из измерений.
На первый взгляд задача кажется тривиальной, но при неправильном подходе время выполнения может растянуться на часы. Разберем, почему стандартные методы работают медленно и как ускорить операцию в сотни раз, используя особенности работы СУБД и индексов.
Посмотрим на исходную ситуацию. У нас есть регистр с двумя измерениями, например, Организация и АкцизнаяМарка. Нам нужно удалить записи по списку акцизных марок. Первое измерение (Организация) нам неизвестно или не используется в отборе.
Часто программисты пишут следующий код:
// Неоптимальный вариант
Для Каждого Марка Из МассивМарок Цикл
Набор = РегистрыСведений.АкцизныеМаркиЕГАИС.СоздатьНаборЗаписей();
Набор.Отбор.АкцизнаяМарка.Установить(Марка);
Набор.Записать();
КонецЦикла;
Почему это работает долго?
В базе данных (SQL) записи физически упорядочены по кластерному индексу. Для регистра сведений кластерный индекс строится по измерениям в порядке их следования в конфигураторе (Измерение1 + Измерение2...).
Когда мы устанавливаем отбор только по второму измерению (АкцизнаяМарка), не указывая первое (Организация), SQL-сервер не может использовать кластерный индекс для быстрого поиска нужной строки. Ему приходится выполнять сканирование всей таблицы (Table Scan) или индекса (Index Scan), перебирая 50 миллионов записей, чтобы найти те, где Марка совпадает с нашей. Это происходит на каждой итерации цикла.
В обсуждении проблемы замеры показали, что удаление одной записи таким способом занимало около 45 секунд. Для 1000 записей это более 12 часов работы.
Чтобы удаление происходило мгновенно, нам нужно заставить СУБД использовать поиск по кластерному индексу (Index Seek). Для этого мы должны указать значения всех измерений, входящих в ключ.
Так как мы знаем только одно измерение, нам нужно сначала выяснить значения остальных. Разобьем решение на два этапа:
Сформируем запрос, который выберет все измерения для нашего списка марок. Даже если в регистре 50 млн записей, выборка по неуникальному индексу (если измерение индексировано) или даже сканирование при чтении выполнится быстрее, чем множественные попытки записи с блокировками.
// Подготовим массив значений для удаления
МассивМарок = Докум.АкцизныеМарки.ВыгрузитьКолонку("АкцизнаяМарка");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| АкцизныеМаркиЕГАИС.ОрганизацияЕГАИС КАК ОрганизацияЕГАИС,
| АкцизныеМаркиЕГАИС.АкцизнаяМарка КАК АкцизнаяМарка
|ИЗ
| РегистрСведений.АкцизныеМаркиЕГАИС КАК АкцизныеМаркиЕГАИС
|ГДЕ
| АкцизныеМаркиЕГАИС.АкцизнаяМарка В(&АкцизнаяМарка)";
Запрос.УстановитьПараметр("АкцизнаяМарка", МассивМарок);
РезультатЗапроса = Запрос.Выполнить();
Теперь, когда у нас есть выборка с полными ключами (и Организация, и АкцизнаяМарка), мы можем удалять записи. Для этого можно использовать МенеджерЗаписи или НаборЗаписей с полным отбором.
Рассмотрим вариант с менеджером записи:
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
МЗ = РегистрыСведений.АкцизныеМаркиЕГАИС.СоздатьМенеджерЗаписи();
// Устанавливаем ВСЕ измерения
МЗ.ОрганизацияЕГАИС = Выборка.ОрганизацияЕГАИС;
МЗ.АкцизнаяМарка = Выборка.АкцизнаяМарка;
Попытка
МЗ.Удалить();
Исключение
Сообщить(ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Такой подход сокращает время операции с 45 секунд до долей секунды (0.006 сек на запись). Ускорение составляет более чем в 170 раз.
В процессе оптимизации может возникнуть вопрос: что работает быстрее — МенеджерЗаписи.Удалить() или НаборЗаписей.Записать()?
Проанализируем техническую сторону. МенеджерЗаписи — это, по сути, удобная обертка. Внутри платформы при вызове метода удаления менеджер записи создает набор записей с отбором по всем ключевым полям и записывает его пустым.
Поэтому код ниже будет работать с той же эффективностью, что и код с менеджером записи, при условии, что мы устанавливаем отбор по всем измерениям:
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
НЗ = РегистрыСведений.АкцизныеМаркиЕГАИС.СоздатьНаборЗаписей();
// Важно: отбор по всем измерениям для попадания в индекс
НЗ.Отбор.ОрганизацияЕГАИС.Установить(Выборка.ОрганизацияЕГАИС);
НЗ.Отбор.АкцизнаяМарка.Установить(Выборка.АкцизнаяМарка);
Попытка
НЗ.Записать();
Исключение
Сообщить(ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Тесты показывают, что разница между этими двумя методами находится на уровне статистической погрешности (0.0064 сек против 0.0066 сек). Главный фактор успеха — наличие полных данных для использования кластерного индекса СУБД.
При работе с такими операциями следует учитывать еще несколько важных моментов:
НачатьТранзакцию() / ЗафиксироватьТранзакцию(). Однако будьте осторожны: одна огромная транзакция на 1000 записей может вызвать блокировку таблицы.Если вам нужно удалить записи из огромного регистра, и вы знаете значение только одного измерения (не первого):