В типовых и нетиповых конфигурациях 1С часто возникает задача преобразования точечных записей периодического регистра сведений в полноценные временные интервалы. Классический пример — периодический регистр сведений СостоянияСотрудников (или кадровые события), где каждая запись фиксирует только факт наступления события (например, «Работает», «В отпуске», «Уволен») и дату его начала (Период). В отчетах же требуется сформировать таблицу с четкими границами интервала: Сотрудник, Состояние, ДатаНачала и ДатаОкончания.
Так как в языке запросов платформы 1С:Предприятие отсутствуют классические оконные функции (такие как LEAD или LAG в SQL), определение границы интервала требует специального подхода. Рассмотрим подробно, как правильно решить эту задачу с помощью языка запросов, средств СКД и программного обхода.
Проанализируем логику вычисления интервала. Дата начала текущего состояния сотрудника берется напрямую из поля Период текущей строки регистра. Датой окончания текущего состояния является момент наступления следующего по времени события того же сотрудника (или за секунду/день до него в зависимости от требуемой дискретности).
Разберем алгоритм построения запроса по шагам:
Состояния.Период < ДатаСледующегоСостояния.Период).МИНИМУМ(ДатаСледующегоСостояния.Период). Это значение и будет датой начала следующего интервала (то есть датой окончания текущего).NULL, что обозначает открытый (действующий по сей день) интервал.Посмотрим на практическую реализацию запроса. В данном примере мы также используем функцию ДОБАВИТЬКДАТЕ, чтобы дата окончания интервала не пересекалась с датой начала следующего состояния, если периодичность регистра задана с точностью до секунды.
ВЫБРАТЬ
Состояния.Сотрудник КАК Сотрудник,
Состояния.Состояние КАК Состояние,
Состояния.Период КАК ДатаНачала,
ВЫБОР
КОГДА МИНИМУМ(ДатаСледующегоСостояния.Период) ЕСТЬ NULL
ТОГДА ДАТАВРЕМЯ(1, 1, 1)
ИНАЧЕ ДОБАВИТЬКДАТЕ(МИНИМУМ(ДатаСледующегоСостояния.Период), СЕКУНДА, -1)
КОНЕЦ КАК ДатаОкончания
ИЗ
РегистрСведений.Состояния КАК Состояния
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Состояния КАК ДатаСледующегоСостояния
ПО Состояния.Сотрудник = ДатаСледующегоСостояния.Сотрудник
И Состояния.Период < ДатаСледующегоСостояния.Период
СГРУППИРОВАТЬ ПО
Состояния.Сотрудник,
Состояния.Период,
Состояния.Состояние
УПОРЯДОЧИТЬ ПО
Сотрудник,
ДатаНачала
Если требуется, чтобы дата окончания содержала точную дату следующего события без вычитания секунды, достаточно оставить в секции выбора МИНИМУМ(ДатаСледующегоСостояния.Период) КАК ДатаОкончания.
Рассмотрим важные технические нюансы работы такого запроса на реальных базах данных:
<) формирует декартово произведение записей для каждого сотрудника. Если у одного сотрудника накопились сотни изменений состояний, такое соединение может выполняться медленно.ПОМЕСТИТЬ), обязательно указывайте директиву ИНДЕКСИРОВАТЬ ПО Сотрудник, Период, чтобы оптимизатор СУБД использовал индексный поиск вместо полного сканирования таблицы.Справочник.Сотрудники, присоединяя к нему интервалы через ЛЕВОЕ СОЕДИНЕНИЕ.Если задача решается в рамках Системы компоновки данных (СКД), можно полностью избежать тяжелых самосоединений в запросе к СУБД. В этом случае запрос выбирает только плоские данные:
ВЫБРАТЬ
Состояния.Сотрудник КАК Сотрудник,
Состояния.Период КАК ДатаНачала,
Состояния.Состояние КАК Состояние
ИЗ
РегистрСведений.Состояния КАК Состояния
Далее на вкладке Вычисляемые поля или во вкладке Ресурсы СКД рассчитываем дату окончания с помощью встроенной функции выражений:
ВычислитьВыражение("ДатаНачала", "Следующая", "Группировка")
Платформа автоматически подставит значение даты из следующей строки группировки в памяти клиента или сервера 1С, что работает значительно быстрее при больших объемах кадровой истории.
В обработках или при прямом формировании табличного документа без СКД задачу можно решить за один линейный проход по предварительно отсортированной выборке, запоминая предыдущую строку в цикле:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Состояния.Сотрудник КАК Сотрудник,
| Состояния.Период КАК Период,
| Состояния.Состояние КАК Состояние
|ИЗ
| РегистрСведений.Состояния КАК Состояния
|УПОРЯДОЧИТЬ ПО
| Состояния.Сотрудник,
| Состояния.Период";
Выборка = Запрос.Выполнить().Выбрать();
ПредыдущаяСтрока = Неопределено;
Пока Выборка.Следующий() Цикл
Если ПредыдущаяСтрока <> Неопределено Тогда
Если ПредыдущаяСтрока.Сотрудник = Выборка.Сотрудник Тогда
// Дата конца предыдущей записи равна дате начала текущей записи
ДатаКонца = Выборка.Период - 1;
Иначе
// У предыдущего сотрудника это была последняя запись (интервал открыт)
ДатаКонца = '00010101';
КонецЕсли;
// Здесь выполняется вывод данных ПредыдущаяСтрока и рассчитанной ДатаКонца
КонецЕсли;
ПредыдущаяСтрока = Новый Структура("Сотрудник, Период, Состояние",
Выборка.Сотрудник, Выборка.Период, Выборка.Состояние);
КонецЦикла;
// Обрабатываем последнюю оставшуюся запись после цикла
Если ПредыдущаяСтрока <> Неопределено Тогда
ДатаКонца = '00010101';
// Вывод последней строки
КонецЕсли;
Данный способ имеет линейную сложность $O(N)$ и гарантирует минимальное время выполнения при программном формировании отчетов со сложной логикой.