При работе с данными о состояниях сотрудников в 1С:Зарплата и Управление Персоналом часто возникает задача аналитического характера: необходимо определить общую продолжительность или крайнюю дату непрерывного периода отсутствия сотрудника. Например, если сотрудник сначала был в основном отпуске, затем сразу же в дополнительном, а потом взял отпуск за свой счет, и все эти события следуют друг за другом без перерыва или с перерывом не более одного дня, нам нужна дата окончания последнего из этих событий как крайняя дата общего периода отсутствия. Если же между событиями возникает "окно" в несколько дней, это означает, что цепочка прерывается, и крайняя дата должна относиться к предыдущей непрерывной последовательности.
Рассмотрим подробнее эту задачу и предложим несколько подходов к ее решению в Системе Компоновки Данных (СКД) 1С, включая как метод, предложенный на форуме, так и более универсальный подход к решению проблемы "дыр и островов" (Gaps and Islands).
Наши исходные данные содержатся в регистре сведений СостоянияСотрудников. Этот регистр хранит информацию о различных состояниях сотрудника на определенные даты, с указанием периода действия:
Сотрудник: Ссылка на элемент справочника Сотрудники.Период: Дата начала состояния.ДействуетДо: Дата окончания состояния.Состояние: Вид состояния (например, отпуск, больничный, работа, увольнение).Нам необходимо сформировать отчет, который для каждого сотрудника определит:
При этом, существуют следующие важные условия:
Работа и Увольнение должны быть исключены.Пример, иллюстрирующий условие:
В этом случае, все три события образуют непрерывную последовательность. Крайняя дата должна быть 20.10.2023, а начало - 01.10.2023.
Здесь последовательность прерывается 11.10.2023. Крайняя дата для первой цепочки будет 10.10.2023, а начало - 01.10.2023. Второе событие (больничный) будет считаться отдельной последовательностью со своим началом 12.10.2023 и окончанием 15.10.2023.
Эта задача является классическим примером проблемы "дыр и островов" (Gaps and Islands) в SQL, которая требует определения непрерывных интервалов в данных, разделенных "дырами" (разрывами).
Для решения задачи "дыр и островов" в языке запросов 1С, который не поддерживает напрямую оконные функции (такие как LAG, LEAD, ROW_NUMBER() OVER (...)), мы обычно используем комбинацию временных таблиц и самосоединений. Общая идея состоит в следующем:
Рассмотрим два подхода к реализации, основываясь на обсуждении в форуме и более надежной реализации метода "дыр и островов".
На форуме было предложено решение, которое автор темы подтвердил как рабочее. Оно использует технику самосоединения и специфический расчет для создания идентификатора группы. Давайте проанализируем его шаги.
Шаг 1: Получение исходных состояний (Временная таблица "Дано")
На первом этапе мы выбираем все состояния сотрудников из регистра СостоянияСотрудников, которые соответствуют нашим критериям: для выбранных сотрудников, после заданной даты и исключая состояния, которые мы не хотим учитывать (например, 'Работа' и 'Увольнение').
ПОМЕСТИТЬ Дано
ВЫБРАТЬ
СостоянияСотрудников.Сотрудник КАК Сотрудник,
СостоянияСотрудников.Период КАК НачалоПериода,
СостоянияСотрудников.ДействуетДо КАК ДействуетДо,
СостоянияСотрудников.Состояние КАК Состояние
ИЗ
РегистрСведений.СостоянияСотрудников КАК СостоянияСотрудников
ГДЕ
СостоянияСотрудников.Сотрудник В(&Сотрудник)
И СостоянияСотрудников.Период > &НачалоПериода // Параметр &НачалоПериода определяет начало отчетного периода
И НЕ СостоянияСотрудников.Состояние В (&ИсключаемыеСостояния) // Здесь &ИсключаемыеСостояния = {ЗНАЧЕНИЕ(Перечисление.СостоянияСотрудника.Работа), ЗНАЧЕНИЕ(Перечисление.СостоянияСотрудника.Увольнение)}
;
Пояснения:
&Сотрудник: Параметр, содержащий список сотрудников, по которым формируется отчет.&НачалоПериода: Параметр, определяющий минимальную дату начала интересующих событий.&ИсключаемыеСостояния: Параметр, содержащий список состояний, которые не должны учитываться (например, "Работа" и "Увольнение").Шаг 2: Расчет кумулятивной длительности (Временная таблица "ЕщёДано")
На этом шаге мы используем самосоединение таблицы "Дано" для вычисления кумулятивной суммы длительностей всех предыдущих и текущих периодов для каждого сотрудника. Это значение сохраняется в поле Разность.
ПОМЕСТИТЬ ЕщёДано
ИЗ
Дано КАК Дано
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Дано КАК Слева
ПО (Слева.НачалоПериода <= Дано.НачалоПериода)
И Дано.Сотрудник = Слева.Сотрудник
СГРУППИРОВАТЬ ПО
Дано.НачалоПериода,
Дано.ДействуетДо,
Дано.Сотрудник
ВЫБРАТЬ
Дано.Сотрудник КАК Сотрудник,
Дано.НачалоПериода КАК НачалоПериода,
Дано.ДействуетДо КАК Конец,
СУММА(РАЗНОСТЬДАТ(Слева.НачалоПериода, Слева.ДействуетДо, ДЕНЬ) + 1) КАК Разность
;
Пояснения:
Дано саму с собой (Дано и Слева).Слева.НачалоПериода <= Дано.НачалоПериода и Дано.Сотрудник = Слева.Сотрудник позволяет для каждой записи из Дано получить все предыдущие и текущие записи для того же сотрудника.СУММА(РАЗНОСТЬДАТ(Слева.НачалоПериода, Слева.ДействуетДо, ДЕНЬ) + 1) вычисляет кумулятивную сумму дней всех периодов, которые начались до или в дату текущего периода для данного сотрудника. Эта Разность будет монотонно возрастать с каждым новым событием.Шаг 3: Агрегация по группам и получение крайних дат
На последнем шаге мы используем вычисленное поле Разность для создания уникального идентификатора группы и агрегируем данные, чтобы получить минимальную дату начала и максимальную дату окончания для каждой такой группы.
ВЫБРАТЬ
Дано.Сотрудник КАК Сотрудник,
МИНИМУМ(Дано.НачалоПериода) КАК НачалоОбъединенногоПериода,
МАКСИМУМ(Дано.Конец) КАК КрайняяДатаОбъединенногоПериода
ИЗ
ЕщёДано КАК Дано
СГРУППИРОВАТЬ ПО
ДОБАВИТЬКДАТЕ(Дано.Конец, ДЕНЬ, -Дано.Разность), // Этот расчет формирует идентификатор группы
Дано.Сотрудник
;
Пояснения:
ДОБАВИТЬКДАТЕ(Дано.Конец, ДЕНЬ, -Дано.Разность). Если периоды идут строго последовательно без разрывов, и Разность является кумулятивной суммой длительностей, то вычитание этой кумулятивной длительности из даты окончания последнего периода в цепочке должно дать одну и ту же "базовую" дату для всех записей в одной непрерывной последовательности.Разность = 10 дней. ИД группы = ДОБАВИТЬКДАТЕ(10.10, ДЕНЬ, -10) = 01.10.Разность = 10 (от первой) + 6 (от второй) = 16 дней. ИД группы = ДОБАВИТЬКДАТЕ(16.10, ДЕНЬ, -16) = 01.10.Разность = 10 + 6 + 4 = 20 дней. ИД группы = ДОБАВИТЬКДАТЕ(20.10, ДЕНЬ, -20) = 01.10.В этом случае, все три записи получают один и тот же ИД группы (01.10), и при агрегации МИНИМУМ(НачалоПериода) даст 01.10, а МАКСИМУМ(Конец) даст 20.10, что соответствует требованию.
Разность = 10 дней. ИД группы = ДОБАВИТЬКДАТЕ(10.10, ДЕНЬ, -10) = 01.10.Разность = 10 (от первой) + 4 (от второй) = 14 дней. ИД группы = ДОБАВИТЬКДАТЕ(15.10, ДЕНЬ, -14) = 01.10.Обратите внимание: Для Сотрудника Б, даже при наличии разрыва в 11.10, этот метод формирует тот же ИД группы (01.10) для обеих записей. Это означает, что данный запрос будет объединять все нерабочие периоды сотрудника в один большой период, не учитывая условие о разрыве в более чем 1 день. Он найдет минимальную дату начала первого отсутствия и максимальную дату окончания последнего отсутствия для всего выбранного интервала, независимо от промежуточных "дыр".
Несмотря на то, что это решение было подтверждено автором как рабочее, из нашего анализа следует, что оно не полностью соответствует первоначальному условию о разрыве более чем в один день. Возможно, в реальных данных автора такие ситуации не встречались, или было допущено некоторое упрощение требований. Если же строгое соблюдение условия "разрыв > 1 дня" критически важно, нам потребуется более детальная реализация метода "дыр и островов".
Для того чтобы строго соблюсти условие, что последовательность прерывается, если разность дат между окончанием одного события и началом следующего составляет более одного дня, нам нужно явно определить эти точки разрыва и на их основе сгенерировать идентификаторы групп. Рассмотрим такой подход пошагово.
Шаг 1: Подготовка и фильтрация исходных данных (Временная таблица "ВТ_ИсходныеДанные")
Как и в первом подходе, мы выбираем все необходимые состояния, фильтруем по сотруднику и периоду, исключаем состояния "Работа" и "Увольнение". Важно отсортировать данные по сотруднику и дате начала для корректного определения предыдущих состояний.
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ВЫБРАТЬ
СостоянияСотрудников.Сотрудник КАК Сотрудник,
СостоянияСотрудников.Период КАК ДатаНачала,
СостоянияСотрудников.ДействуетДо КАК ДатаОкончания
ИЗ
РегистрСведений.СостоянияСотрудников КАК СостоянияСотрудников
ГДЕ
СостоянияСотрудников.Сотрудник В(&Сотрудник)
И СостоянияСотрудников.Состояние <> ЗНАЧЕНИЕ(Перечисление.СостоянияСотрудника.Работа)
И СостоянияСотрудников.Состояние <> ЗНАЧЕНИЕ(Перечисление.СостоянияСотрудника.Увольнение)
// Дополнительные фильтры по периоду отчета
И (СостоянияСотрудников.Период МЕЖДУ &НачалоПериода И &ОкончаниеПериода
ИЛИ СостоянияСотрудников.ДействуетДо МЕЖДУ &НачалоПериода И &ОкончаниеПериода
ИЛИ (СостоянияСотрудников.Период <= &НачалоПериода И СостоянияСотрудников.ДействуетДо >= &ОкончаниеПериода)) // Учитываем периоды, пересекающиеся с отчетным
УПОРЯДОЧИТЬ ПО
Сотрудник, ДатаНачала;
Шаг 2: Определение точек разрыва (Временная таблица "ВТ_СТочкамиРазрыва")
На этом шаге мы для каждой записи определяем, является ли она началом новой непрерывной группы. Для этого мы находим дату окончания предыдущего по времени состояния того же сотрудника и сравниваем ее с датой начала текущего состояния.
ПОМЕСТИТЬ ВТ_СТочкамиРазрыва
ВЫБРАТЬ
Т1.Сотрудник КАК Сотрудник,
Т1.ДатаНачала КАК ДатаНачала,
Т1.ДатаОкончания КАК ДатаОкончания,
ВЫБОР
КОГДА Т1.ДатаНачала > ДОБАВИТЬКДАТЕ(
ЕСТЬNULL(
(ВЫБРАТЬ МАКСИМУМ(Т2.ДатаОкончания)
ИЗ ВТ_ИсходныеДанные КАК Т2
ГДЕ Т2.Сотрудник = Т1.Сотрудник
И Т2.ДатаНачала < Т1.ДатаНачала), // Находим все состояния, которые начались строго ДО текущего
ДАТАВРЕМЯ(1,1,1) // Минимальная дата для первого состояния сотрудника
), ДЕНЬ, 1)
ТОГДА 1 // Если текущее событие начинается более чем через 1 день после предыдущего, это разрыв
ИНАЧЕ 0 // Иначе это продолжение предыдущей группы
КОНЕЦ КАК ПризнакРазрыва
ИЗ
ВТ_ИсходныеДанные КАК Т1
УПОРЯДОЧИТЬ ПО
Сотрудник, ДатаНачала;
Пояснения:
МАКСИМУМ(Т2.ДатаОкончания) среди всех предыдущих записей для того же сотрудника (Т2.ДатаНачала < Т1.ДатаНачала). Это эквивалентно функции LAG(), но реализовано через самосоединение с агрегацией.ЕСТЬNULL(..., ДАТАВРЕМЯ(1,1,1)): Для первого события сотрудника, у которого нет предыдущих записей, МАКСИМУМ(Т2.ДатаОкончания) будет NULL. Мы заменяем его минимальной датой, чтобы условие Т1.ДатаНачала > ДОБАВИТЬКДАТЕ(..., ДЕНЬ, 1) всегда было истинным для первой записи, тем самым помечая ее как начало новой группы.ПризнакРазрыва: Это поле будет равно 1, если текущее событие является началом новой последовательности (есть разрыв с предыдущим), и 0, если оно продолжает предыдущую последовательность.Шаг 3: Расчет кумулятивного счетчика разрывов для получения ИД группы (Временная таблица "ВТ_СГруппами")
Теперь, когда у нас есть флаг ПризнакРазрыва, мы можем создать уникальный идентификатор для каждой группы. Для этого мы подсчитываем, сколько раз ПризнакРазрыва = 1 встретился до текущей записи (включительно) для каждого сотрудника. Этот кумулятивный счетчик будет служить НомерГруппы.
ПОМЕСТИТЬ ВТ_СГруппами
ВЫБРАТЬ
Т1.Сотрудник КАК Сотрудник,
Т1.ДатаНачала КАК ДатаНачала,
Т1.ДатаОкончания КАК ДатаОкончания,
СУММА(Т2.ПризнакРазрыва) КАК НомерГруппы
ИЗ
ВТ_СТочкамиРазрыва КАК Т1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_СТочкамиРазрыва КАК Т2
ПО Т1.Сотрудник = Т2.Сотрудник
И Т2.ДатаНачала <= Т1.ДатаНачала // Суммируем все ПризнакРазрыва до текущей записи
СГРУППИРОВАТЬ ПО
Т1.Сотрудник,
Т1.ДатаНачала,
Т1.ДатаОкончания
УПОРЯДОЧИТЬ ПО
Сотрудник, ДатаНачала;
Пояснения:
ВТ_СТочкамиРазрыва.Т2.ДатаНачала <= Т1.ДатаНачала позволяет для каждой записи Т1 учесть все записи Т2, которые начались до или в ту же дату.СУММА(Т2.ПризнакРазрыва): Это кумулятивная сумма флагов разрыва. Для каждой записи это даст порядковый номер группы, к которой она принадлежит. Если запись является частью первой группы, НомерГруппы будет 1. Если она относится ко второй группе (после первого разрыва), НомерГруппы будет 2, и так далее.Шаг 4: Агрегация по группам для получения крайних дат (Финальная выборка)
Теперь, когда каждая запись отнесена к своей уникальной группе (по Сотрудник и НомерГруппы), мы можем легко агрегировать данные, чтобы найти минимальную дату начала и максимальную дату окончания для каждой такой непрерывной последовательности.
ВЫБРАТЬ
ВТ_СГруппами.Сотрудник КАК Сотрудник,
МИНИМУМ(ВТ_СГруппами.ДатаНачала) КАК НачалоОбъединенногоПериода,
МАКСИМУМ(ВТ_СГруппами.ДатаОкончания) КАК КрайняяДатаОбъединенногоПериода
ИЗ
ВТ_СГруппами КАК ВТ_СГруппами
СГРУППИРОВАТЬ ПО
ВТ_СГруппами.Сотрудник,
ВТ_СГруппами.НомерГруппы
УПОРЯДОЧИТЬ ПО
Сотрудник, НачалоОбъединенногоПериода;
Пояснения:
Сотрудник и НомерГруппы гарантирует, что мы объединяем только те записи, которые относятся к одной и той же непрерывной последовательности отсутствий.МИНИМУМ(ВТ_СГруппами.ДатаНачала): Даст дату начала первого события в объединенной последовательности.МАКСИМУМ(ВТ_СГруппами.ДатаОкончания): Даст крайнюю дату последнего события в объединенной последовательности.Пример трассировки для Сотрудника Б с разрывом (01.10-10.10, 12.10-15.10) с использованием надежного подхода:
ПредыдущаяДатаОкончания = ДАТАВРЕМЯ(1,1,1). ПризнакРазрыва = 1 (так как 01.10 > 01.01.0001 + 1 день).ПредыдущаяДатаОкончания = 10.10.2023. ПризнакРазрыва = 1 (так как 12.10 > 10.10.2023 + 1 день).НомерГруппы = 1 (считается только ПризнакРазрыва самой первой записи).НомерГруппы = 2 (считается ПризнакРазрыва первой и второй записи: 1+1=2).НачалоОбъединенногоПериода = 01.10.2023, КрайняяДатаОбъединенногоПериода = 10.10.2023.НачалоОбъединенногоПериода = 12.10.2023, КрайняяДатаОбъединенногоПериода = 15.10.2023.Этот результат полностью соответствует требуемому по условию "разрыв > 1 дня".
Для использования любого из предложенных запросов в СКД, вам необходимо выполнить следующие шаги:
&Сотрудник, &НачалоПериода, &ОкончаниеПериода, &ИсключаемыеСостояния) на закладке "Параметры" СКД, при необходимости задав им значения по умолчанию или установив их как пользовательские параметры.НачалоОбъединенногоПериода и КрайняяДатаОбъединенногоПериода, если вы хотите их использовать как ресурсы (например, для вычисления длительности).&НачалоПериода и &ОкончаниеПериода должны влиять на выборку данных. Запрос должен учитывать события, которые начались до &НачалоПериода, но продолжаются внутри отчетного интервала, или заканчиваются после &ОкончаниеПериода, но начались внутри.СостоянияСотрудников обычно не позволяет создавать перекрывающиеся записи для одного сотрудника).Мы рассмотрели задачу получения крайней даты последовательно идущих состояний отсутствия сотрудника, исключая "Работу" и "Увольнение", с учетом условия о прерывании последовательности при наличии разрыва более одного дня. Был представлен запрос с форума, который, как выяснилось, объединяет все нерабочие периоды сотрудника в один, независимо от промежуточных разрывов. Для строгого соблюдения условия о "разрыве > 1 дня" был предложен более надежный и универсальный запрос, реализующий метод "дыр и островов" путем определения точек разрыва и нумерации групп. Выбор подхода зависит от ваших точных требований к логике объединения периодов и специфики ваших данных.