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

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

При разработке отчетов и доработке конфигураций 1С программисты часто сталкиваются с задачей сопоставления текущего и предыдущего состояний объекта. Типичный пример — необходимость вывести кадровые изменения сотрудников в плоском формате: Сотрудник / Новая должность / Новый отдел / Предыдущая должность / Предыдущий отдел. Такая информация обычно хранится в периодическом регистре сведений КадроваяИсторияСотрудников.

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

Типичные ошибки при решении задачи

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

Ошибка №1: Использование виртуальной таблицы СрезПоследних

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

Ошибка №2: Ограничение ВЫБРАТЬ ПЕРВЫЕ 2

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

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

Метод 1: Классический алгоритм через поиск предпоследнего периода

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

Шаг 1. Получение текущего состояния сотрудников. Сначала нам нужно определить последнюю актуальную дату кадрового изменения для каждого сотрудника. Для этого мы обратимся к срезу последних или сгруппируем физическую таблицу по сотрудникам и найдем максимум периода. Результат поместим во временную таблицу.

Шаг 2. Поиск предпоследнего периода. Мы берем нашу временную таблицу с актуальными датами и соединяем её левым соединением с физической таблицей регистра. Условие связи: тот же сотрудник, но период в физической таблице должен быть строго меньше текущего периода. Сгруппировав результат по сотруднику, мы находим максимум из этих «меньших» периодов — это и будет дата предпоследнего кадрового события.

Шаг 3. Финальное объединение данных в одну строку. Получив две даты (текущую и предыдущую), мы дважды левым соединением подключаем к ним физическую таблицу регистра сведений, вытаскивая нужные кадровые реквизиты (подразделение, должность) для каждого момента времени.

Посмотрим на пример реализации этого алгоритма в виде готового запроса 1С:


ВЫБРАТЬ
	КадроваяИсторияСотрудниковСрезПоследних.Сотрудник КАК Сотрудник,
	КадроваяИсторияСотрудниковСрезПоследних.Период КАК ПериодТекущий
ПОМЕСТИТЬ ВТ_ТекущиеПериоды
ИЗ
	РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(&Период, ) КАК КадроваяИсторияСотрудниковСрезПоследних
;

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

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

Обратите внимание на использование функции ЕСТЬNULL в финальном пакете запроса. Если у сотрудника в истории числится всего одна запись (например, он был только что принят на работу и перемещений не имел), то левое соединение для поиска предыдущего периода вернет NULL. Данная функция позволяет корректно обработать такие ситуации и вернуть пустые ссылки вместо пустых значений СУБД.

Метод 2: Эмуляция оконной функции ROW_NUMBER()

В современных СУБД (MS SQL, PostgreSQL) подобные задачи изящно решаются с помощью оконных функций: ROW_NUMBER() OVER (PARTITION BY Сотрудник ORDER BY Период DESC). Эта функция пронумеровала бы все кадровые события для каждого сотрудника по порядку, после чего нам осталось бы просто выбрать строки с номерами 1 и 2.

Полноценных оконных функций в платформе 1С:Предприятие нет. Однако мы можем эмулировать это поведение с помощью связи таблицы самой с собой. Проанализируем, как это сделать.

Если мы соединим таблицу кадровой истории саму с собой по условию совпадения сотрудника и отношению периодов РС2.Период >= РС1.Период, то для каждого кадрового изменения мы сможем подсчитать количество записей, которые лежат в будущем по отношению к нему. Для самой последней записи это количество будет равно 1, для предпоследней — 2, для пред-предпоследней — 3 и так далее. Это и есть порядковый номер строки с конца истории.

Рассмотрим пример реализации эмуляции нумерации строк:


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

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

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

Метод 3: Использование архитектурных особенностей ЗУП 3.1 и ERP

Если ваша задача решается в конфигурациях «Зарплата и управление персоналом» (ЗУП 3.1), «Комплексная автоматизация» (КА 2) или «ERP Управление предприятием 2», то изобретать сложные велосипеды с запросами не всегда обязательно. Разработчики фирмы 1С уже заложили в эти системы мощные механизмы для работы с кадровой историей.

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

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

Пример использования API конфигурации для подготовки кадровых данных:


// 1. Создадим массив сотрудников, по которым нам нужны данные
МассивСотрудников = Новый Массив;
МассивСотрудников.Добавить(СсылкаНаСотрудника);

// 2. Опишем список необходимых полей
ПоляЗапроса = "Сотрудник, Подразделение, Должность, Период";

// 3. Вызовем функцию общего модуля для получения таблицы актуальных кадровых данных
ТаблицаКадров = КадровыйУчет.ПодготовитьТаблицуСотрудников(МассивСотрудников, ПоляЗапроса, ТекущаяДата);

Использование этого интерфейса гарантирует, что при обновлении конфигурации ваш код не сломается из-за изменения структуры регистров сведений, так как вы обращаетесь к стабильному программному слою, поддерживаемому разработчиками фирмы 1С.

Выбор конкретного метода зависит от архитектуры вашей конфигурации и объема обрабатываемых данных. Для самописных конфигураций оптимальным выбором будет Метод 1. Для сложных типовых решений на базе ERP и ЗУП наиболее надежным и простым в поддержке решением станет использование встроенного программного интерфейса кадрового учета.

← На главную