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

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

При разработке отчетов и печатных форм в конфигурации «1С:Зарплата и управление персоналом» часто требуется получить актуальные кадровые данные сотрудника (подразделение, должность, ставку) и одновременно узнать его текущее состояние (работает, находится в отпуске, на больничном или в командировке). Попытка объединить два отдельных запроса в один нередко приводит к трудноуловимым ошибкам и потере строк в отчете. Разберем типичные ошибки, проанализируем причины их возникновения и рассмотрим правильные варианты реализации задачи.

Анализ проблемы и разбор критических ошибок соединения

Рассмотрим исходную ситуацию: у нас есть срез последних записей регистра сведений КадроваяИсторияСотрудников и данные из регистра СостоянияСотрудников. На форумах часто предлагают связать эти таблицы внутренним соединением с условием совпадения периодов, например:


ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СостоянияСотрудников КАК СостоянияСотрудников
ПО КадроваяИсторияСотрудниковСрезПоследних.Период = СостоянияСотрудников.Период
    И КадроваяИсторияСотрудниковСрезПоследних.Сотрудник = СостоянияСотрудников.Сотрудник

Разберем, почему такая конструкция является грубой ошибкой и в реальной базе выдаст пустой или неполный результат:

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

Вариант 1. Соединение данных через пакет запросов и временные таблицы

Проанализируем оптимальный способ построения чистого запроса без привлечения внутренних программных функций. Разберем алгоритм по шагам:

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

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


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

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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    МаксДаты.Сотрудник КАК Сотрудник,
    Состояния.Состояние КАК Состояние
ПОМЕСТИТЬ ВТ_АктуальныеСостояния
ИЗ
    ВТ_МаксПериодыСостояний КАК МаксДаты
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СостоянияСотрудников КАК Состояния
        ПО МаксДаты.Сотрудник = Состояния.Сотрудник
            И МаксДаты.Период = Состояния.Период
;

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

В этом запросе данные кадровой истории сохраняются в полном объеме, а состояние сотрудника подтягивается корректно на указанный момент времени независимо от даты кадрового перемещения.

Вариант 2. Использование штатного программного интерфейса ЗУП 3.1 (рекомендуемый)

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

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


// Формируем список требуемых полей через запятую
ПоляКадров = "Сотрудник,ФизическоеЛицо,ТабельныйНомер,Подразделение,Должность,КоличествоСтавок,ВидЗанятости,Состояние";

// Вызываем типовую функцию получения данных
ТаблицаДанных = КадровыйУчет.КадровыеДанныеСотрудников(
    Истина, 
    ПараметрСотрудник, 
    ПоляКадров, 
    КонецПериода(НачалоПериода, "ДЕНЬ")
);

Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
    // Доступны все запрошенные поля, включая состояние и кадровые реквизиты
    ТекущееСостояние = СтрокаДанных.Состояние;
    ТекущаяДолжность = СтрокаДанных.Должность;
КонецЦикла;

Если же данные требуются внутри сложного пользовательского запроса, процедура КадровыйУчет.СоздатьТаблицуКадровыхДанных позволяет сформировать и наполнить временную таблицу кадровых данных прямо в переданном объекте МенеджерВременныхТаблиц, исключая необходимость вручную разбирать интервалы и состояния в текстах запросов.

← На главную