В практике разработки на платформе 1С:Предприятие 8 часто возникает необходимость получить данные из периодического регистра сведений на определенную дату. Если дата среза одна, мы используем виртуальную таблицу СрезПоследних. Однако задача существенно усложняется, когда нужно получить срез последних для целого списка объектов (например, сотрудников) на разные даты или когда стандартная виртуальная таблица не подходит по условиям задачи. В таких случаях программисты прибегают к «ручному» срезу последних через временные таблицы, что нередко приводит к ошибке «двоения» (дублирования) строк.
Рассмотрим подробно, почему возникают дубли, и разберем правильный алгоритм построения запроса на примере регистра ГражданствоФизЛиц.
Проанализируем типичную ошибку. Когда мы пытаемся получить срез последних вручную, мы часто создаем временную таблицу, в которую выбираем измерения регистра и используем функцию МАКСИМУМ(Период). Ошибка заключается в том, что в секцию СГРУППИРОВАТЬ ПО включаются не только измерения, но и ресурсы регистра (например, Страна, ВидЗастрахованногоЛица).
Если у физического лица в истории изменений были разные значения страны или статуса, то группировка по этим полям выдаст несколько строк для одного человека, так как комбинации «Физлицо + Страна + Статус» будут разными. Даже функция МАКСИМУМ не поможет, так как она найдет максимальную дату для каждой уникальной комбинации полей, указанных в группировке.
Разберем это на примере: если сотрудник сменил гражданство с «Украина» на «Россия», и мы сгруппировали запрос по физлицу и стране, мы получим две строки: максимальную дату, когда он был гражданином Украины, и максимальную дату, когда он стал гражданином России.
Для корректного получения данных необходимо придерживаться строгой трехшаговой структуры запроса. Рассмотрим этот алгоритм подробнее.
Шаг 1. Получение максимальных дат (периодов)
Сначала мы должны создать временную таблицу, которая содержит только ключевые поля (измерения) и максимальный период для каждого ключа. На этом этапе категорически нельзя выбирать ресурсы (страну, статус и т.д.).
ВЫБРАТЬ
ГражданствоФизЛиц.ФизЛицо КАК ФизЛицо,
МАКСИМУМ(ГражданствоФизЛиц.Период) КАК Период
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
РегистрСведений.ГражданствоФизЛиц КАК ГражданствоФизЛиц
ГДЕ
ГражданствоФизЛиц.Период <= &ДатаСреза
СГРУППИРОВАТЬ ПО
ГражданствоФизЛиц.ФизЛицо
ИНДЕКСИРОВАТЬ ПО
ФизЛицо,
Период
;
Шаг 2. Получение данных (ресурсов) через соединение
Теперь, когда у нас есть уникальные пары «Физлицо — Максимальный Период», мы соединяем эту временную таблицу с физической таблицей регистра, чтобы «подтянуть» остальные данные.
ВЫБРАТЬ
МаксДаты.ФизЛицо,
ГражданствоДанные.Страна,
ГражданствоДанные.ВидЗастрахованногоЛица,
ГражданствоДанные.Период
ИЗ
ВТ_МаксимальныеДаты КАК МаксДаты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ГражданствоФизЛиц КАК ГражданствоДанные
ПО МаксДаты.ФизЛицо = ГражданствоДанные.ФизЛицо
И МаксДаты.Период = ГражданствоДанные.Период
Важное примечание: Если регистр периодический в пределах секунды, и существует вероятность записи двух разных значений в одну и ту же секунду (что является ошибкой проектирования, но встречается), запрос снова может «задвоить». В таких случаях в соединение добавляют дополнительные поля, такие как НомерСтроки или Регистратор, также предварительно получив их максимум на первом шаге.
Выясним причину, по которой нельзя просто передать дату из другой таблицы в параметры виртуальной таблицы СрезПоследних. Платформа 1С не поддерживает динамическую подстановку даты в параметры виртуальной таблицы внутри одного запроса. Если у нас есть временная таблица ВТ_Периоды (например, список расчетных месяцев), и нам нужно получить статус сотрудника на конец каждого месяца, мы используем следующий подход:
ВТ_Периоды с физической таблицей регистра по условию Регистр.Период <= ВТ_Периоды.ДатаКонцаМесяца.ВТ_Периоды.ДатаКонцаМесяца и Регистр.ФизЛицо.МАКСИМУМ(Регистр.Период).Пример реализации такого «километрового» запроса для расчета страховых взносов (фрагмент):
ВЫБРАТЬ
ВТПериоды.Период КАК ПериодРасчета,
Гражданство.ФизЛицо КАК ФизЛицо,
МАКСИМУМ(Гражданство.Период) КАК ПериодСреза
ПОМЕСТИТЬ ВТ_СрезНаКаждыйМесяц
ИЗ
ВТПериоды КАК ВТПериоды
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ГражданствоФизЛиц КАК Гражданство
ПО Гражданство.ФизЛицо В (ВЫБРАТЬ СписокФизЛиц.ФизЛицо ИЗ ВТ_СписокФизЛиц КАК СписокФизЛиц)
И Гражданство.Период <= ВТПериоды.Период
СГРУППИРОВАТЬ ПО
ВТПериоды.Период,
Гражданство.ФизЛицо
;
В типовых конфигурациях (УПП, ЗУП 2.5) данный регистр имеет поле ДатаРегистрацииИзменений. Важно понимать разницу: Период — это стандартное поле, определяющее время вступления записи в силу, а ДатаРегистрацииИзменений — это вспомогательный реквизит (или ресурс), который может использоваться в специфических отчетах. Всегда проверяйте структуру регистра: если срез нужно получить по логике программы, используйте Период. Если вы пытаетесь построить срез по реквизиту ДатаРегистрацииИзменений, группировки должны быть еще более строгими.
Проанализируем ситуацию с PostgreSQL. Некоторые пользователи отмечают, что виртуальные таблицы на этой СУБД могут работать иначе, чем на MS SQL. Использование физических таблиц с явным поиском максимума (как описано выше) — это самый надежный и универсальный способ, гарантирующий идентичный результат на любой СУБД.
Чтобы ваш запрос никогда не «двоил» при получении последних данных из регистра, следуйте этим правилам:
Использование этого подхода сделает ваши запросы стабильными, а данные — достоверными.