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

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

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

В чем причина такого поведения СрезаПоследних

Проанализируем исходный запрос разработчика:


Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ВыбПериод", НачалоГода(Источник.Дата));
Запрос.УстановитьПараметр("ВыбСобственник", Источник.Кос_Собственник);
Запрос.УстановитьПараметр("ВыбУдаленный", Ложь);
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
|	Кос_НомераСчетовСрезПоследних.НомерЧислом
|ИЗ
|	РегистрСведений.Кос_НомераСчетов.СрезПоследних(
|			&ВыбПериод,
|			Собственник = &ВыбСобственник
|				И Удаленный = &ВыбУдаленный) КАК Кос_НомераСчетовСрезПоследних
|
|УПОРЯДОЧИТЬ ПО
|	Кос_НомераСчетовСрезПоследних.НомерЧислом УБЫВ";

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

Выясним причину попадания прошлогодней записи в результат:

  1. Когда мы передаем в параметр &ВыбПериод значение НачалоГода(Источник.Дата) (например, 01.01.2024 00:00:00), платформа ищет самую актуальную запись, сделанную на эту дату или ранее.
  2. Если в текущем году записей еще не было, последней существующей записью для переданных измерений оказывается запись прошлого года. Именно ее виртуальная таблица и возвращает как срез последних данных на момент начала года.
  3. Кроме того, виртуальная таблица СрезПоследних группирует данные по уникальным комбинациям измерений регистра. Она возвращает самую свежую по времени запись для конкретной комбинации измерений, а вовсе не строку с максимальным значением ресурса НомерЧислом.

Решение 1. Запрос к физической таблице с использованием функции МАКСИМУМ

Если перед нами стоит задача имитации нумератора — найти максимальный номер с отбором по собственнику и за конкретный год — использовать срез последних категорически не подходит. Обратимся напрямую к физической (реальной) таблице регистра сведений РегистрСведений.Кос_НомераСчетов.

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


Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	ЕСТЬNULL(МАКСИМУМ(Кос_НомераСчетов.НомерЧислом), 0) КАК МаксимальныйНомер
|ИЗ
|	РегистрСведений.Кос_НомераСчетов КАК Кос_НомераСчетов
|ГДЕ
|	Кос_НомераСчетов.Период МЕЖДУ &НачалоПериода И &КонецПериода
|	И Кос_НомераСчетов.Собственник = &ВыбСобственник
|	И Кос_НомераСчетов.Удаленный = &ВыбУдаленный";

Запрос.УстановитьПараметр("НачалоПериода", НачалоГода(Источник.Дата));
Запрос.УстановитьПараметр("КонецПериода", КонецГода(Источник.Дата));
Запрос.УстановитьПараметр("ВыбСобственник", Источник.Кос_Собственник);
Запрос.УстановитьПараметр("ВыбУдаленный", Ложь);

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

Разберем ключевые детали этой реализации:

  1. Диапазон дат вместо точного равенства: Конструкция Период МЕЖДУ &НачалоПериода И &КонецПериода ограничивает выборку рамками нужного года. Значение КонецГода(...) возвращает точное окончание года (31.12.ГГГГ 23:59:59).
  2. Отказ от функций в условиях: Не следует писать в условии ГОД(Период) = &Год. Применение функций к полям таблиц в блоке ГДЕ мешает СУБД использовать стандартные индексы по полю Период, что провоцирует полное сканирование таблицы (Full Table Scan).
  3. Агрегатная функция МАКСИМУМ: Вместо конструкции ВЫБРАТЬ ПЕРВЫЕ 1 ... УПОРЯДОЧИТЬ ПО ... УБЫВ эффективнее использовать агрегатную функцию МАКСИМУМ. Обертка ЕСТЬNULL(..., 0) сразу возвращает 0, если записей за текущий год еще нет, избавляя от лишних проверок на NULL в коде.
  4. Индексация полей: Поля, участвующие в секции ГДЕ (Собственник, Удаленный), должны быть измерениями регистра либо иметь включенное свойство Индексировать, чтобы запрос выполнялся быстро даже при миллионах строк.

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

Рассмотрим риски, возникающие при имитации нумератора через запросы. В многопользовательской среде простой поиск максимального номера с последующей записью нового значения в регистр подвержен «состоянию гонки» (Race Condition). Если два пользователя одновременно создадут документ, оба запроса прочитают один и тот же максимальный номер и попытаются создать дубликаты.

Для предотвращения коллизий рекомендуем учесть следующие архитектурные варианты:

  1. Использование управляемых блокировок: Если вычисление номера выполняется вручную, получение максимального номера и запись нового значения обязательно должны происходить внутри транзакции с предварительной установкой исключительной блокировки через объект БлокировкаДанных по измерениям регистра и периоду.
  2. Отдельный регистр счетчиков: Вместо поиска максимума по всему массиву записей можно создать непериодический регистр сведений (измерения: Собственник, Год; ресурс: ТекущийНомер). При генерации номера в транзакции считывается число, прибавляется единица и перезаписывается обратно в регистр. Это исключает перебор исторических данных.
  3. Штатные нумераторы 1С: Если нумерация строится для документов или справочников конфигурации, надежнее использовать стандартный функционал платформы — свойство Периодичность номера («В пределах года») или объект метаданных Нумератор. Разделение по организациям или собственникам в типовых механизмах платформы достигается указанием реквизита номератора или разделением номеров по реквизиту.
← На главную