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

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

В типовых и нетиповых конфигурациях 1С часто возникает задача преобразования точечных записей периодического регистра сведений в полноценные временные интервалы. Классический пример — периодический регистр сведений СостоянияСотрудников (или кадровые события), где каждая запись фиксирует только факт наступления события (например, «Работает», «В отпуске», «Уволен») и дату его начала (Период). В отчетах же требуется сформировать таблицу с четкими границами интервала: Сотрудник, Состояние, ДатаНачала и ДатаОкончания.

Так как в языке запросов платформы 1С:Предприятие отсутствуют классические оконные функции (такие как LEAD или LAG в SQL), определение границы интервала требует специального подхода. Рассмотрим подробно, как правильно решить эту задачу с помощью языка запросов, средств СКД и программного обхода.

Принцип решения задачи через самосоединение таблицы в запросе

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

Разберем алгоритм построения запроса по шагам:

  1. Выбираем базовую таблицу регистра сведений с текущими состояниями.
  2. Выполняем левое соединение регистра с самим собой по тому же сотруднику, но с условием по дате: период присоединенной таблицы должен быть строго больше периода текущей записи (Состояния.Период < ДатаСледующегоСостояния.Период).
  3. Для каждой строки группируем результат и находим минимальную дату из всех будущих событий с помощью агрегатной функции МИНИМУМ(ДатаСледующегоСостояния.Период). Это значение и будет датой начала следующего интервала (то есть датой окончания текущего).
  4. Для последнего актуального состояния сотрудника будущих записей не найдется, поэтому левое соединение вернет значение NULL, что обозначает открытый (действующий по сей день) интервал.

Пример запроса для получения интервалов состояний

Посмотрим на практическую реализацию запроса. В данном примере мы также используем функцию ДОБАВИТЬКДАТЕ, чтобы дата окончания интервала не пересекалась с датой начала следующего состояния, если периодичность регистра задана с точностью до секунды.


ВЫБРАТЬ
    Состояния.Сотрудник КАК Сотрудник,
    Состояния.Состояние КАК Состояние,
    Состояния.Период КАК ДатаНачала,
    ВЫБОР
        КОГДА МИНИМУМ(ДатаСледующегоСостояния.Период) ЕСТЬ NULL
            ТОГДА ДАТАВРЕМЯ(1, 1, 1)
        ИНАЧЕ ДОБАВИТЬКДАТЕ(МИНИМУМ(ДатаСледующегоСостояния.Период), СЕКУНДА, -1)
    КОНЕЦ КАК ДатаОкончания
ИЗ
    РегистрСведений.Состояния КАК Состояния
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Состояния КАК ДатаСледующегоСостояния
        ПО Состояния.Сотрудник = ДатаСледующегоСостояния.Сотрудник
            И Состояния.Период < ДатаСледующегоСостояния.Период
СГРУППИРОВАТЬ ПО
    Состояния.Сотрудник,
    Состояния.Период,
    Состояния.Состояние
УПОРЯДОЧИТЬ ПО
    Сотрудник,
    ДатаНачала

Если требуется, чтобы дата окончания содержала точную дату следующего события без вычитания секунды, достаточно оставить в секции выбора МИНИМУМ(ДатаСледующегоСостояния.Период) КАК ДатаОкончания.

Особенности производительности и обработка больших объемов данных

Рассмотрим важные технические нюансы работы такого запроса на реальных базах данных:

  1. Нагрузка на СУБД: Соединение таблицы самой с собой по условию неравенства (<) формирует декартово произведение записей для каждого сотрудника. Если у одного сотрудника накопились сотни изменений состояний, такое соединение может выполняться медленно.
  2. Индексация: Если данные предварительно помещаются во временную таблицу (ПОМЕСТИТЬ), обязательно указывайте директиву ИНДЕКСИРОВАТЬ ПО Сотрудник, Период, чтобы оптимизатор СУБД использовал индексный поиск вместо полного сканирования таблицы.
  3. Сотрудники без истории: Если в итоговый отчет необходимо вывести абсолютно всех сотрудников из справочника (включая тех, по которым еще нет записей в регистре), в качестве главной таблицы запроса следует использовать Справочник.Сотрудники, присоединяя к нему интервалы через ЛЕВОЕ СОЕДИНЕНИЕ.

Альтернативный вариант: расчет следующей даты средствами СКД

Если задача решается в рамках Системы компоновки данных (СКД), можно полностью избежать тяжелых самосоединений в запросе к СУБД. В этом случае запрос выбирает только плоские данные:


ВЫБРАТЬ
    Состояния.Сотрудник КАК Сотрудник,
    Состояния.Период КАК ДатаНачала,
    Состояния.Состояние КАК Состояние
ИЗ
    РегистрСведений.Состояния КАК Состояния

Далее на вкладке Вычисляемые поля или во вкладке Ресурсы СКД рассчитываем дату окончания с помощью встроенной функции выражений:

ВычислитьВыражение("ДатаНачала", "Следующая", "Группировка")

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

Альтернативный вариант: программный обход выборки на встроенном языке

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Состояния.Сотрудник КАК Сотрудник,
    |	Состояния.Период КАК Период,
    |	Состояния.Состояние КАК Состояние
    |ИЗ
    |	РегистрСведений.Состояния КАК Состояния
    |УПОРЯДОЧИТЬ ПО
    |	Состояния.Сотрудник,
    |	Состояния.Период";

Выборка = Запрос.Выполнить().Выбрать();

ПредыдущаяСтрока = Неопределено;

Пока Выборка.Следующий() Цикл
    Если ПредыдущаяСтрока <> Неопределено Тогда
        Если ПредыдущаяСтрока.Сотрудник = Выборка.Сотрудник Тогда
            // Дата конца предыдущей записи равна дате начала текущей записи
            ДатаКонца = Выборка.Период - 1;
        Иначе
            // У предыдущего сотрудника это была последняя запись (интервал открыт)
            ДатаКонца = '00010101';
        КонецЕсли;
        
        // Здесь выполняется вывод данных ПредыдущаяСтрока и рассчитанной ДатаКонца
    КонецЕсли;
    
    ПредыдущаяСтрока = Новый Структура("Сотрудник, Период, Состояние", 
        Выборка.Сотрудник, Выборка.Период, Выборка.Состояние);
КонецЦикла;

// Обрабатываем последнюю оставшуюся запись после цикла
Если ПредыдущаяСтрока <> Неопределено Тогда
    ДатаКонца = '00010101';
    // Вывод последней строки
КонецЕсли;

Данный способ имеет линейную сложность $O(N)$ и гарантирует минимальное время выполнения при программном формировании отчетов со сложной логикой.

← На главную