При разработке отчетов и кадровых выгрузок часто возникает задача: получить не просто актуальное состояние сотрудника, а сопоставить его с предыдущим кадровым состоянием. Например, нам нужно вывести строку вида: «Сотрудник | Последняя должность | Предпоследняя должность». Стандартная виртуальная таблица СрезПоследних возвращает только одну, самую свежую запись. Если же мы попытаемся выбрать первые две записи с сортировкой по убыванию даты, то получим две строки в таблице значений, что крайне неудобно для дальнейшего вывода в плоские отчеты или печатные формы.
В этой статье мы подробно разберем несколько эффективных способов решения этой задачи на уровне запроса 1С. Мы начнем с универсальных приемов работы с платформой, а в конце рассмотрим особенности и "подводные камни", специфичные для конфигурации 1С:Зарплата и управление персоналом (ЗУП) 3.1.
Этот метод считается одним из самых изящных и производительных. Мы последовательно получаем два среза последних: сначала самый актуальный, а затем — предпоследний, исключив из выборки период последней записи.
Давайте разберем этот процесс по шагам:
СрезПоследних регистра сведений КадроваяИсторияСотрудников на нужную дату и помещаем результат во временную таблицу.СрезПоследних, но в параметрах виртуальной таблицы (в секции условий) указываем, что период записи не должен совпадать с периодом, полученным на Шаге 1.Посмотрим на практическую реализацию этого запроса:
// Шаг 1. Получаем самую последнюю запись кадровой истории
ВЫБРАТЬ
СрезПоследний.Сотрудник КАК Сотрудник,
СрезПоследний.Период КАК ПериодПоследний,
СрезПоследний.Должность КАК ДолжностьПоследняя
ПОМЕСТИТЬ ВТ_ПоследниеЗаписи
ИЗ
РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(&Период, ) КАК СрезПоследний
;
// Шаг 2. Получаем предпоследнюю запись, исключив период последней записи
ВЫБРАТЬ
СрезПредпоследний.Сотрудник КАК Сотрудник,
СрезПредпоследний.Период КАК ПериодПредпоследний,
СрезПредпоследний.Должность КАК ДолжностьПредпоследняя
ПОМЕСТИТЬ ВТ_ПредпоследниеЗаписи
ИЗ
РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(
&Период,
НЕ (Сотрудник, Период) В (
ВЫБРАТЬ
ВТ.Сотрудник,
ВТ.ПериодПоследний
ИЗ
ВТ_ПоследниеЗаписи КАК ВТ
)
) КАК СрезПредпоследний
;
// Шаг 3. Соединяем результаты в одну строку
ВЫБРАТЬ
Последние.Сотрудник КАК Сотрудник,
Последние.ДолжностьПоследняя КАК ДолжностьПоследняя,
ПРЕДСТАВЛЕНИЕ(Последние.ДолжностьПоследняя) КАК ДолжностьПоследняяПредставление,
ЕСТЬNULL(Предпоследние.ДолжностьПредпоследняя, ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка)) КАК ДолжностьПредпоследняя,
ПРЕДСТАВЛЕНИЕ(Предпоследние.ДолжностьПредпоследняя) КАК ДолжностьПредпоследняяПредставление
ИЗ
ВТ_ПоследниеЗаписи КАК Последние
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПредпоследниеЗаписи КАК Предпоследние
ПО Последние.Сотрудник = Предпоследние.Сотрудник
Преимущество метода: Использование индексов виртуальных таблиц СУБД, что обеспечивает высокую скорость выполнения запроса даже на базах с большим объемом кадровых данных.
Если в вашей базе данных использование конструкции НЕ В в параметрах виртуальной таблицы вызывает просадки производительности на SQL-сервере, мы можем использовать альтернативный подход с поиском максимальных дат напрямую через физическую таблицу регистра.
Проанализируем этот алгоритм:
Рассмотрим пример реализации такого запроса:
// Шаг 1. Находим дату последнего кадрового события
ВЫБРАТЬ
КадроваяИстория.Сотрудник КАК Сотрудник,
МАКСИМУМ(КадроваяИстория.Период) КАК ПериодПоследний
ПОМЕСТИТЬ ВТ_МаксимальныйПериод
ИЗ
РегистрСведений.КадроваяИсторияСотрудников КАК КадроваяИстория
ГДЕ
КадроваяИстория.Период <= &Период
ГРУППИРОВАТЬ ПО
КадроваяИстория.Сотрудник
;
// Шаг 2. Находим дату предпоследнего кадрового события
ВЫБРАТЬ
КадроваяИстория.Сотрудник КАК Сотрудник,
МАКСИМУМ(КадроваяИстория.Период) КАК ПериодПредпоследний
ПОМЕСТИТЬ ВТ_ПредпоследнийПериод
ИЗ
РегистрСведений.КадроваяИсторияСотрудников КАК КадроваяИстория
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_МаксимальныйПериод КАК МаксПериод
ПО КадроваяИстория.Сотрудник = МаксПериод.Сотрудник
И КадроваяИстория.Период < МаксПериод.ПериодПоследний
ГДЕ
КадроваяИстория.Период <= &Период
ГРУППИРОВАТЬ ПО
КадроваяИстория.Сотрудник
;
// Шаг 3. Соединяем даты с реальными данными из физической таблицы
ВЫБРАТЬ
МаксПериод.Сотрудник КАК Сотрудник,
РегистрПоследний.Должность КАК ДолжностьПоследняя,
ЕСТЬNULL(РегистрПредпоследний.Должность, ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка)) КАК ДолжностьПредпоследняя
ИЗ
ВТ_МаксимальныйПериод КАК МаксПериод
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПредпоследнийПериод КАК ПредПериод
ПО МаксПериод.Сотрудник = ПредПериод.Сотрудник
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудников КАК РегистрПоследний
ПО МаксПериод.Сотрудник = РегистрПоследний.Сотрудник
И МаксПериод.ПериодПоследний = РегистрПоследний.Период
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудников КАК РегистрПредпоследний
ПО ПредПериод.Сотрудник = РегистрПредпоследний.Сотрудник
И ПредПериод.ПериодПредпоследний = РегистрПредпоследний.Период
Этот способ часто выручает, когда нужно собрать данные из разнородных таблиц или быстро "развернуть" строки в колонки без сложных левых соединений. Идея заключается в том, чтобы присвоить каждой строке свой признак-колонку и объединить их, заполнив противоположные поля значениями NULL, а затем выполнить группировку.
Давайте посмотрим, как можно реализовать эту логику в запросе:
ВЫБРАТЬ
Сотрудник КАК Сотрудник,
МАКСИМУМ(ДолжностьПоследняя) КАК ДолжностьПоследняя,
МАКСИМУМ(ДолжностьПредпоследняя) КАК ДолжностьПредпоследняя
ИЗ (
// Выбираем только последние данные
ВЫБРАТЬ
ВТ_Последние.Сотрудник КАК Сотрудник,
ВТ_Последние.ДолжностьПоследняя КАК ДолжностьПоследняя,
ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка) КАК ДолжностьПредпоследняя
ИЗ
ВТ_ПоследниеЗаписи КАК ВТ_Последние
ОБЪЕДИНИТЬ ВСЕ
// Выбираем только предпоследние данные
ВЫБРАТЬ
ВТ_Предпоследние.Сотрудник КАК Сотрудник,
ЗНАЧЕНИЕ(Справочник.Должности.ПустаяСсылка) КАК ДолжностьПоследняя,
ВТ_Предпоследние.ДолжностьПредпоследняя КАК ДолжностьПредпоследняя
ИЗ
ВТ_ПредпоследниеЗаписи КАК ВТ_Предпоследние
) КАК ОбъединенныеДанные
ГРУППИРОВАТЬ ПО
Сотрудник
Свертка через агрегатную функцию МАКСИМУМ отсекает пустые ссылки и оставляет в итоговой строке заполненные значения для обеих колонок.
Если вы решаете эту задачу в конфигурации 1С:Зарплата и управление персоналом 3.1 (ЗУП), использование прямых запросов к регистру сведений КадроваяИсторияСотрудников является грубой методологической ошибкой. Это связано со спецификой учета временных кадровых переводов в системе.
Выясним причину:
КадроваяИсторияСотрудников в дату окончания перевода не создается новая запись! В результате обычный запрос или СрезПоследних вернет вам временную должность как актуальную, даже если срок перевода давно истек.КадроваяИсторияСотрудниковИнтервальный). Данные в нем разбиты на непрерывные временные интервалы с учетом вытеснения. Писать к нему прямые запросы вручную крайне сложно из-за специфики хранения времени окончания периодов (например, секунды вида 23:59:58).Для гарантированного получения корректных кадровых данных следует использовать стандартные программные механизмы БСП (Библиотеки Стандартных Подсистем), заложенные разработчиками 1С:
СписокСотрудников = Новый Массив;
СписокСотрудников.Добавить(ВыбранныйСотрудник);
КадровыеДанные = КадровыйУчет.КадровыеДанныеСотрудников(
Истина, // Получать только актуальные данные
СписокСотрудников,
"Подразделение, Должность",
РабочаяДата
);
ЗарплатаКадрыОбщиеНаборыДанных. Это позволит вам использовать в схеме компоновки виртуальные таблицы представлений (например, Представления_СрезПоследних_КадроваяИсторияСотрудников), которые вернут абсолютно корректную и "чистую" историю изменений без ошибок временных переводов.Таким образом, мы рассмотрели несколько универсальных путей решения проблемы на уровне СУБД с помощью временных таблиц, а также научились обходить методологические ловушки при работе со спецификой конфигурации ЗУП 3.1.