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

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

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

В этой статье мы подробно разберем несколько эффективных способов решения этой задачи на уровне запроса 1С. Мы начнем с универсальных приемов работы с платформой, а в конце рассмотрим особенности и "подводные камни", специфичные для конфигурации 1С:Зарплата и управление персоналом (ЗУП) 3.1.

Способ 1. Исключение последнего периода через виртуальную таблицу СрезПоследних

Этот метод считается одним из самых изящных и производительных. Мы последовательно получаем два среза последних: сначала самый актуальный, а затем — предпоследний, исключив из выборки период последней записи.

Давайте разберем этот процесс по шагам:

  1. Шаг 1. Получение последней актуальной записи. Обращаемся к виртуальной таблице СрезПоследних регистра сведений КадроваяИсторияСотрудников на нужную дату и помещаем результат во временную таблицу.
  2. Шаг 2. Получение предпоследней записи. Снова обращаемся к таблице СрезПоследних, но в параметрах виртуальной таблицы (в секции условий) указываем, что период записи не должен совпадать с периодом, полученным на Шаге 1.
  3. Шаг 3. Соединение данных. Левым соединением связываем обе временные таблицы по сотруднику и выводим результат в одну строку.

Посмотрим на практическую реализацию этого запроса:


// Шаг 1. Получаем самую последнюю запись кадровой истории
ВЫБРАТЬ
    СрезПоследний.Сотрудник КАК Сотрудник,
    СрезПоследний.Период КАК ПериодПоследний,
    СрезПоследний.Должность КАК ДолжностьПоследняя
ПОМЕСТИТЬ ВТ_ПоследниеЗаписи
ИЗ
    РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(&Период, ) КАК СрезПоследний
;

// Шаг 2. Получаем предпоследнюю запись, исключив период последней записи
ВЫБРАТЬ
    СрезПредпоследний.Сотрудник КАК Сотрудник,
    СрезПредпоследний.Период КАК ПериодПредпоследний,
    СрезПредпоследний.Должность КАК ДолжностьПредпоследняя
ПОМЕСТИТЬ ВТ_ПредпоследниеЗаписи
ИЗ
    РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(
        &Период,
        НЕ (Сотрудник, Период) В (
            ВЫБРАТЬ 
                ВТ.Сотрудник, 
                ВТ.ПериодПоследний 
            ИЗ 
                ВТ_ПоследниеЗаписи КАК ВТ
        )
    ) КАК СрезПредпоследний
;

// Шаг 3. Соединяем результаты в одну строку
ВЫБРАТЬ
    Последние.Сотрудник КАК Сотрудник,
    Последние.ДолжностьПоследняя КАК ДолжностьПоследняя,
    ПРЕДСТАВЛЕНИЕ(Последние.ДолжностьПоследняя) КАК ДолжностьПоследняяПредставление,
    ЕСТЬNULL(Предпоследние.ДолжностьПредпоследняя, ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка)) КАК ДолжностьПредпоследняя,
    ПРЕДСТАВЛЕНИЕ(Предпоследние.ДолжностьПредпоследняя) КАК ДолжностьПредпоследняяПредставление
ИЗ
    ВТ_ПоследниеЗаписи КАК Последние
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПредпоследниеЗаписи КАК Предпоследние
        ПО Последние.Сотрудник = Предпоследние.Сотрудник

Преимущество метода: Использование индексов виртуальных таблиц СУБД, что обеспечивает высокую скорость выполнения запроса даже на базах с большим объемом кадровых данных.

Способ 2. Получение периодов через физическую таблицу (Двойной максимум)

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

Проанализируем этот алгоритм:

  1. Сначала мы находим максимальную дату движения для каждого сотрудника на дату среза. Это будет дата последнего события.
  2. Затем мы ищем второй максимум — наибольшую дату движения, которая строго меньше первой найденной даты. Это будет дата предпоследнего события.
  3. После этого мы соединяем полученные даты с физической таблицей регистра для получения самих кадровых данных (должностей, подразделений и т.д.).

Рассмотрим пример реализации такого запроса:


// Шаг 1. Находим дату последнего кадрового события
ВЫБРАТЬ
    КадроваяИстория.Сотрудник КАК Сотрудник,
    МАКСИМУМ(КадроваяИстория.Период) КАК ПериодПоследний
ПОМЕСТИТЬ ВТ_МаксимальныйПериод
ИЗ
    РегистрСведений.КадроваяИсторияСотрудников КАК КадроваяИстория
ГДЕ
    КадроваяИстория.Период <= &Период
ГРУППИРОВАТЬ ПО
    КадроваяИстория.Сотрудник
;

// Шаг 2. Находим дату предпоследнего кадрового события
ВЫБРАТЬ
    КадроваяИстория.Сотрудник КАК Сотрудник,
    МАКСИМУМ(КадроваяИстория.Период) КАК ПериодПредпоследний
ПОМЕСТИТЬ ВТ_ПредпоследнийПериод
ИЗ
    РегистрСведений.КадроваяИсторияСотрудников КАК КадроваяИстория
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_МаксимальныйПериод КАК МаксПериод
        ПО КадроваяИстория.Сотрудник = МаксПериод.Сотрудник
        И КадроваяИстория.Период < МаксПериод.ПериодПоследний
ГДЕ
    КадроваяИстория.Период <= &Период
ГРУППИРОВАТЬ ПО
    КадроваяИстория.Сотрудник
;

// Шаг 3. Соединяем даты с реальными данными из физической таблицы
ВЫБРАТЬ
    МаксПериод.Сотрудник КАК Сотрудник,
    РегистрПоследний.Должность КАК ДолжностьПоследняя,
    ЕСТЬNULL(РегистрПредпоследний.Должность, ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка)) КАК ДолжностьПредпоследняя
ИЗ
    ВТ_МаксимальныйПериод КАК МаксПериод
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПредпоследнийПериод КАК ПредПериод
        ПО МаксПериод.Сотрудник = ПредПериод.Сотрудник
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудников КАК РегистрПоследний
        ПО МаксПериод.Сотрудник = РегистрПоследний.Сотрудник
        И МаксПериод.ПериодПоследний = РегистрПоследний.Период
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудников КАК РегистрПредпоследний
        ПО ПредПериод.Сотрудник = РегистрПредпоследний.Сотрудник
        И ПредПериод.ПериодПредпоследний = РегистрПредпоследний.Период

Способ 3. Использование объединений (UNION ALL) и сворачивание через МАКСИМУМ

Этот способ часто выручает, когда нужно собрать данные из разнородных таблиц или быстро "развернуть" строки в колонки без сложных левых соединений. Идея заключается в том, чтобы присвоить каждой строке свой признак-колонку и объединить их, заполнив противоположные поля значениями NULL, а затем выполнить группировку.

Давайте посмотрим, как можно реализовать эту логику в запросе:


ВЫБРАТЬ
    Сотрудник КАК Сотрудник,
    МАКСИМУМ(ДолжностьПоследняя) КАК ДолжностьПоследняя,
    МАКСИМУМ(ДолжностьПредпоследняя) КАК ДолжностьПредпоследняя
ИЗ (
    // Выбираем только последние данные
    ВЫБРАТЬ
        ВТ_Последние.Сотрудник КАК Сотрудник,
        ВТ_Последние.ДолжностьПоследняя КАК ДолжностьПоследняя,
        ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка) КАК ДолжностьПредпоследняя
    ИЗ
        ВТ_ПоследниеЗаписи КАК ВТ_Последние

    ОБЪЕДИНИТЬ ВСЕ

    // Выбираем только предпоследние данные
    ВЫБРАТЬ
        ВТ_Предпоследние.Сотрудник КАК Сотрудник,
        ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка) КАК ДолжностьПоследняя,
        ВТ_Предпоследние.ДолжностьПредпоследняя КАК ДолжностьПредпоследняя
    ИЗ
        ВТ_ПредпоследниеЗаписи КАК ВТ_Предпоследние
) КАК ОбъединенныеДанные

ГРУППИРОВАТЬ ПО
    Сотрудник

Свертка через агрегатную функцию МАКСИМУМ отсекает пустые ссылки и оставляет в итоговой строке заполненные значения для обеих колонок.

Важная специфика ЗУП 3.1: Почему прямые запросы к кадровой истории могут выдавать ошибку?

Если вы решаете эту задачу в конфигурации 1С:Зарплата и управление персоналом 3.1 (ЗУП), использование прямых запросов к регистру сведений КадроваяИсторияСотрудников является грубой методологической ошибкой. Это связано со спецификой учета временных кадровых переводов в системе.

Выясним причину:

Как решать задачу в ЗУП 3.1 правильно?

Для гарантированного получения корректных кадровых данных следует использовать стандартные программные механизмы БСП (Библиотеки Стандартных Подсистем), заложенные разработчиками 1С:

  1. Программное получение через КадровыйУчет: Если вам нужно получить актуальные кадровые данные сотрудников в коде модуля, используйте готовую функцию общего модуля. Она сама учтет все интервалы, переводы и исправления документов:
    
    СписокСотрудников = Новый Массив;
    СписокСотрудников.Добавить(ВыбранныйСотрудник);
    
    КадровыеДанные = КадровыйУчет.КадровыеДанныеСотрудников(
        Истина, // Получать только актуальные данные
        СписокСотрудников, 
        "Подразделение, Должность", 
        РабочаяДата
    );
    
  2. Использование Представлений в СКД: Если вы строите отчет на базе Системы компоновки данных (СКД), инициализируйте в модуле объекта заполнение временных таблиц через общий модуль ЗарплатаКадрыОбщиеНаборыДанных. Это позволит вам использовать в схеме компоновки виртуальные таблицы представлений (например, Представления_СрезПоследних_КадроваяИсторияСотрудников), которые вернут абсолютно корректную и "чистую" историю изменений без ошибок временных переводов.

Таким образом, мы рассмотрели несколько универсальных путей решения проблемы на уровне СУБД с помощью временных таблиц, а также научились обходить методологические ловушки при работе со спецификой конфигурации ЗУП 3.1.

← На главную