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

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

При разработке конфигураций на платформе 1С:Предприятие перед разработчиками регулярно возникает задача получения максимального значения какого-либо поля. Классический пример — ручная генерация уникального номера по порядку для записей в независимом регистре сведений. Чаще всего для решения этой задачи рассматривают два варианта запроса: использование агрегатной функции МАКСИМУМ или выборка ПЕРВЫЕ 1 с сортировкой в обратном порядке. В этой статье мы подробно проанализируем, какой из этих подходов работает быстрее, как на их выполнение влияют индексы СУБД и какие критические опасности таит в себе схема нумерации "Максимальный + 1" в условиях многопользовательской работы.

Два подхода к получению максимума в запросах 1С

Рассмотрим два варианта написания запроса для нашего регистра сведений, где измерением является индексируемое поле НомерПоПорядку.

Вариант 1. Использование агрегатной функции МАКСИМУМ

Этот вариант выглядит максимально естественно и лаконично. Мы просто просим СУБД вернуть нам наибольшее значение поля из таблицы:


ВЫБРАТЬ
    МАКСИМУМ(Рег.НомерПоПорядку) КАК МаксНомер
ИЗ
    РегистрСведений.ИмяРегистра КАК Рег

Вариант 2. Использование ПЕРВЫЕ 1 и сортировки УБЫВ

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


ВЫБРАТЬ ПЕРВЫЕ 1
    Рег.НомерПоПорядку КАК МаксНомер
ИЗ
    РегистрСведений.ИмяРегистра КАК Рег
УПОРЯДОЧИТЬ ПО
    МаксНомер УБЫВ

Давайте разберем по шагам, как ведут себя эти запросы в различных условиях работы СУБД и платформы.

Анализ работы запросов на уровне СУБД

Чтобы понять, какой запрос отработает быстрее, нам необходимо спуститься на уровень СУБД (будь то MS SQL Server, PostgreSQL или Oracle) и проанализировать планы выполнения запросов.

Сценарий 1: Поле индексировано (штатная ситуация)

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

  1. При выполнении запроса с ПЕРВЫЕ 1 ... УПОРЯДОЧИТЬ ПО ... УБЫВ оптимизатор СУБД выполняет обратное сканирование индекса (Backward Scan). Он мгновенно переходит к последней странице индекса, считывает ровно одну запись (самую "правую" в дереве индекса) и сразу же останавливает выполнение запроса. Стоимость такой операции ничтожно мала — СУБД читает всего одну страницу данных.
  2. При выполнении запроса с функцией МАКСИМУМ умный оптимизатор СУБД понимает, что поле индексировано, и преобразует этот запрос в аналогичный точечный поиск по индексу. План выполнения будет идентичен первому варианту.

Таким образом, при наличии актуальной статистики и индекса оба запроса отработают с одинаковой (максимальной) скоростью.

Важное исключение (поведение оптимизатора): Практика администрирования баз данных показывает, что оптимизаторы СУБД (особенно MS SQL Server) иногда могут давать сбой при использовании агрегатных функций вроде MAX(). При устаревшей статистике, неявном преобразовании типов или наличии null-значений оптимизатор может построить неоптимальный план и вместо быстрого поиска выполнить полное сканирование индекса (Index Scan) с последующей группировкой. В то же время конструкция с TOP 1 ... ORDER BY DESC практически всегда отрабатывает стабильно и предсказуемо. Поэтому многие специалисты считают второй вариант более надежным и "железобетонным".

Сценарий 2: Поле НЕ индексировано (теоретический случай)

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

Специфика платформы 1С:Предприятие

Помимо чистой работы СУБД, нам необходимо учитывать особенности трансляции запросов самой платформой 1С.

Проблема составных типов данных

Если измеряемое поле имеет составной тип данных (например, число и ссылка на документ), использование функции МАКСИМУМ может преподнести неприятный сюрприз. Платформа 1С при трансляции такого запроса сгенерирует сложную SQL-конструкцию с операторами CASE WHEN для каждого типа. СУБД не сможет эффективно применить индекс к такому выражению. В то же время запрос с ПЕРВЫЕ 1 ... УПОРЯДОЧИТЬ ПО транслируется гораздо проще и продолжает использовать индекс СУБД напрямую.

Удобство получения сопутствующих данных

В реальной разработке нам часто нужно получить не просто сухой номер, а всю строку целиком (например, узнать автора последней записи или дату её создания). При использовании ПЕРВЫЕ 1 мы можем легко расширить список выбираемых полей:


ВЫБРАТЬ ПЕРВЫЕ 1
    Рег.НомерПоПорядку КАК МаксНомер,
    Рег.Автор,
    Рег.Период
ИЗ
    РегистрСведений.ИмяРегистра КАК Рег
УПОРЯДОЧИТЬ ПО
    МаксНомер УБЫВ

Если же мы решим использовать агрегатную функцию МАКСИМУМ, нам придется либо группировать запрос по остальным полям (что полностью исказит результат), либо писать тяжелый подзапрос со связями по максимальному значению.

Критическая проблема: Конкурентный доступ и дублирование номеров

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

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

  1. Пользователь А запускает транзакцию. Его сеанс выполняет запрос получения максимального номера. База возвращает число 100.
  2. В эту же миллисекунду Пользователь Б запускает свою транзакцию. Его запрос тоже считывает данные из таблицы и возвращает число 100 (так как Пользователь А еще не записал и не зафиксировал свою строку).
  3. Пользователь А рассчитывает номер: 100 + 1 = 101 и успешно записывает строку.
  4. Пользователь Б рассчитывает номер: 100 + 1 = 101 и пытается записать строку. Происходит либо замена данных Пользователя А (если это ключевое поле), либо система выдает ошибку нарушения уникальности.

Для предотвращения этой проблемы в 1С применяются два основных архитектурных паттерна.

Решение 1. Использование управляемых блокировок

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

Проанализируем пример правильной реализации:


НачатьТранзакцию();

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

// Теперь мы защищены от параллельного чтения. Выполняем запрос:
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ ПЕРВЫЕ 1
    |	Рег.НомерПоПорядку КАК МаксНомер
    |ИЗ
    |	РегистрСведений.ИмяВашегоРегистра КАК Рег
    |УПОРЯДОЧИТЬ ПО
    |	НомерПоПорядку УБЫВ";

РезультатЗапроса = Запрос.Выполнить();
НовыйНомер = 1;

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

// Выполняем запись новой строки в регистр с рассчитанным НовыйНомер
// ...

ЗафиксироватьТранзакцию();

При таком подходе второй сеанс, дойдя до строки Блокировка.Заблокировать(), послушно остановится и будет ожидать, пока первая транзакция полностью завершится (запишет данные и сделает ЗафиксироватьТранзакцию()). Только после этого он получит актуальный максимальный номер 101 и создаст запись с номером 102.

Решение 2. Создание вспомогательного регистра-нумератора

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

В этом случае отличным архитектурным решением является создание второго, крайне легковесного регистра сведений (например, РегистрСведений.МаксимальныеНомера). В нем будет храниться всего одна запись со значением текущего счетчика.

При добавлении новой записи мы:

  1. Блокируем только одну эту строку во вспомогательном регистре.
  2. Считываем текущее значение, прибавляем единицу.
  3. Записываем новое значение обратно в регистр-нумератор.
  4. Используем полученное число для записи в наш основной тяжелый регистр сведений.

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

Выводы и рекомендации

Подведем итоги нашего совместного исследования:

← На главную