SQL Вопрос о пропусках / островах - Определите, проработал ли кто-то X лет без перерыва Y дней - PullRequest
1 голос
/ 06 августа 2020

Работа над проблемой для компании в Японии. У правительства есть некоторые правила, такие как ... Если у вас рабочая виза:

  1. Вы не можете работать более 3 лет в компании , не взяв 30 дней отпуска
  2. Вы не можете работать в одной и той же кадровой компании более 5 лет, не взяв 6 месяцев отпуска

Итак, мы хотим выяснить, будет ли кто-либо нарушать какое-либо правило в следующие 30 лет / 60/90 дней.

Пример данных (список контрактов):

if object_id('tempdb..#sampleDates') is not null drop table #sampleDates
create table #sampleDates (UserId int, CompanyID int, WorkPeriodStart datetime, WorkPeriodEnd datetime)
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27809, 972, '2019-10-10', '2020-10-10')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27853, 484, '2019-10-10', '2020-10-10')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27856, 172, '2019-10-10', '2020-10-10')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2015-01-01', '2015-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2016-01-01', '2017-02-28')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2017-01-01', '2017-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2018-01-01', '2018-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2019-01-01', '2020-01-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2020-01-01', '2020-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27897, 179, '2019-10-10', '2020-10-10')

Моя первая проблема, возможно, перекрытие дат. Я уже близок к решению этого вопроса, но пока я не знаю, как решить проблему «Рабочие X лет / Y выходных дней», я не уверен, как должен выглядеть результат моей таблицы cte или temp.

Я не ожидаю, что кто-то сделает эту работу за меня, но я хочу найти статью, которая может сказать мне:

  • Как я могу определить, делал ли кто-то перерывы в период времени, и как долго (промежутки между диапазонами дат)?
  • Как я могу определить, проработают ли они 3/5 лет без перерыва 30/180 дней в следующие 30/60/90 дней?

Это казалось таким простым, пока я не начал кодировать процедуру.

Заранее спасибо за любую помощь.

EDIT:

Как бы то ни было, вот моя вторая рабочая попытка по устранению перекрывающихся дат (в первой версии использовался подход density_rank, и он работал, пока я что-то не напортачил, выбрал что-то простое):


;with CJ as (
    select UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd from #sampleDates c 
)
select 
       c.CompanyID,
       c.WorkPeriodStart,
       min(t1.WorkPeriodEnd) as EndDate
from CJ c
inner join CJ t1 on c.WorkPeriodStart <= t1.WorkPeriodEnd and c.UserId = t1.UserId and c.CompanyID = t1.CompanyID
    and not exists(select * from CJ t2 where t1.UserId = t2.UserId and t1.CompanyID = t2.CompanyID and t1.WorkPeriodEnd >= t2.WorkPeriodStart AND t1.WorkPeriodEnd < t2.WorkPeriodEnd) 
where not exists(select * from CJ c2 where c.UserId = c2.UserId and c.CompanyID = c2.CompanyID and c.WorkPeriodStart > c2.WorkPeriodStart AND c.WorkPeriodStart <= c2.WorkPeriodEnd) 

group by c.UserId, c.CompanyID, c.WorkPeriodStart 
order by c.UserId, c.WorkPeriodStart 

Ответы [ 3 ]

1 голос
/ 06 августа 2020

Этот скрипт объединяет любые перекрывающиеся рабочие периоды, а затем вычисляет общее количество дней, проработанных за предыдущие 3- и 5-летние периоды. Затем берет это значение и определяет, превышает ли оно максимально допустимое количество рабочих дней в течение этого периода на UserId и CompanyId для 3-летнего лимита и только на UserId для 5-летнего лимита. (Это правильная интерпретация правил в вашем вопросе?)

Отсюда он просто добавляет 30, 60 и 90 дней к этой сумме, чтобы увидеть, будет ли это большее значение быть за соответствующими пределами. Учитывая разные правила группировки, это было бы чище, как 2 запроса (без дублирования UserId для 5-летнего правила), но результат по-прежнему является флагом против любого нарушающего UserId.

В приведенном ниже примере могут видеть, что UserId = 27857 только нарушает правило 5 лет в настоящее время, но затем также нарушает правило 3 лет, если они останутся на еще 60 дней. Кроме того, UserId = 27858 в настоящее время приемлемо, но будет нарушать правило 5 лет через 60 дней.

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

Script

if object_id('tempdb..#sampleDates') is not null drop table #sampleDates
create table #sampleDates (UserId int, CompanyId int, WorkPeriodStart datetime, WorkPeriodEnd datetime)
insert #sampleDates values
 (27809, 972, '2019-10-10', '2020-10-10')
,(27853, 484, '2019-10-10', '2020-10-10')
,(27856, 172, '2019-10-10', '2020-10-10')

,(27857, 1234, '2015-01-01', '2015-12-31')
,(27857, 1234, '2016-01-01', '2017-02-28')
,(27857, 1234, '2017-01-01', '2017-12-31')
,(27857, 1234, '2018-01-01', '2018-12-31')
,(27857, 1234, '2019-01-01', '2020-01-31')
,(27857, 1234, '2020-01-01', '2020-05-31')

,(27858, 1234, '2015-01-01', '2015-12-31')
,(27858, 1234, '2016-01-01', '2017-02-28')
,(27858, 1234, '2017-01-01', '2017-12-31')
,(27858, 1234, '2018-01-01', '2018-12-31')
,(27858, 1234, '2019-09-01', '2020-01-31')
,(27858, 1234, '2020-01-01', '2020-08-31')

,(27859, 12345, '2015-01-01', '2015-12-31')
,(27859, 12346, '2016-01-01', '2017-02-28')
,(27859, 12347, '2017-01-01', '2017-12-31')
,(27859, 12348, '2018-01-01', '2018-12-31')
,(27859, 12349, '2019-01-01', '2020-01-31')
,(27859, 12340, '2020-01-01', '2020-12-31')

,(27897, 179, '2019-10-10', '2020-10-10')
;

declare @3YearsAgo date = dateadd(year,-3,getdate());
declare @3YearWorkingDays int = (365*3)-30;

declare @5YearsAgo date = dateadd(year,-5,getdate());
declare @5YearWorkingDays int = (365*5)-(365/2);

with p as
(
    select UserId
          ,CompanyId
          ,min(WorkPeriodStart) as WorkPeriodStart
          ,max(WorkPeriodEnd) as WorkPeriodEnd
    from(select l.*,
                sum(case when dateadd(day,1,l.PrevEnd) < l.WorkPeriodStart then 1 else 0 end) over (partition by l.UserId, l.CompanyId order by l.WorkPeriodStart rows unbounded preceding) as grp
        from(select d.*,
                    lag(d.WorkPeriodEnd) over (partition by d.UserId, d.CompanyId order by d.WorkPeriodEnd) as PrevEnd
            from #sampleDates as d
            ) as l
        ) as g
    group by grp
            ,UserId
            ,CompanyId
)
,d as
(
    select UserId
          ,CompanyId
          ,sum(case when @3YearsAgo < WorkPeriodEnd
                    then datediff(day
                                 ,case when @3YearsAgo between WorkPeriodStart and WorkPeriodEnd then @3YearsAgo else WorkPeriodStart end
                                 ,WorkPeriodEnd
                                 )
                    else 0
                    end
              ) as WorkingDays3YearsToToday
          
          ,sum(case when @5YearsAgo < WorkPeriodEnd
                    then datediff(day
                                 ,case when @5YearsAgo between WorkPeriodStart and WorkPeriodEnd then @5YearsAgo else WorkPeriodStart end
                                 ,WorkPeriodEnd
                                 )
                    else 0
                    end
               ) as WorkingDays5YearsToToday
    from p
    group by UserId
            ,CompanyId
)
select UserId
     ,CompanyId
     ,@3YearWorkingDays as Limit3Year
     ,@5YearWorkingDays as Limit5Year
     ,WorkingDays3YearsToToday
     ,WorkingDays5YearsToToday

     ,case when WorkingDays3YearsToToday > @3YearWorkingDays then 1 else 0 end as Violation3YearNow
     ,case when sum(WorkingDays5YearsToToday) over (partition by UserId) > @5YearWorkingDays then 1 else 0 end as Violation5YearNow
     
     ,case when WorkingDays3YearsToToday + 30 > @3YearWorkingDays then 1 else 0 end as Violation3Year30Day
     ,case when sum(WorkingDays5YearsToToday) over (partition by UserId) + 30 > @5YearWorkingDays then 1 else 0 end as Violation5Year30Day

     ,case when WorkingDays3YearsToToday + 60 > @3YearWorkingDays then 1 else 0 end as Violation3Year60Day
     ,case when sum(WorkingDays5YearsToToday) over (partition by UserId) + 60 > @5YearWorkingDays then 1 else 0 end as Violation5Year60Day

     ,case when WorkingDays3YearsToToday + 90 > @3YearWorkingDays then 1 else 0 end as Violation3Year90Day
     ,case when sum(WorkingDays5YearsToToday) over (partition by UserId) + 90 > @5YearWorkingDays then 1 else 0 end as Violation5Year90Day
from d
order by UserId
        ,CompanyId;

Output

+--------+-----------+------------+------------+--------------------------+--------------------------+-------------------+-------------------+---------------------+---------------------+---------------------+---------------------+---------------------+---------------------+
| UserId | CompanyId | Limit3Year | Limit5Year | WorkingDays3YearsToToday | WorkingDays5YearsToToday | Violation3YearNow | Violation5YearNow | Violation3Year30Day | Violation5Year30Day | Violation3Year60Day | Violation5Year60Day | Violation3Year90Day | Violation5Year90Day |
+--------+-----------+------------+------------+--------------------------+--------------------------+-------------------+-------------------+---------------------+---------------------+---------------------+---------------------+---------------------+---------------------+
|  27809 |       972 |       1065 |       1643 |                      366 |                      366 |                 0 |                 0 |                   0 |                   0 |                   0 |                   0 |                   0 |                   0 |
|  27853 |       484 |       1065 |       1643 |                      366 |                      366 |                 0 |                 0 |                   0 |                   0 |                   0 |                   0 |                   0 |                   0 |
|  27856 |       172 |       1065 |       1643 |                      366 |                      366 |                 0 |                 0 |                   0 |                   0 |                   0 |                   0 |                   0 |                   0 |
|  27857 |      1234 |       1065 |       1643 |                     1029 |                     1760 |                 0 |                 1 |                   0 |                   1 |                   1 |                   1 |                   1 |                   1 |
|  27858 |      1234 |       1065 |       1643 |                      877 |                     1608 |                 0 |                 0 |                   0 |                   0 |                   0 |                   1 |                   0 |                   1 |
|  27859 |     12340 |       1065 |       1643 |                      365 |                      365 |                 0 |                 1 |                   0 |                   1 |                   0 |                   1 |                   0 |                   1 |
|  27859 |     12345 |       1065 |       1643 |                        0 |                      147 |                 0 |                 1 |                   0 |                   1 |                   0 |                   1 |                   0 |                   1 |
|  27859 |     12346 |       1065 |       1643 |                        0 |                      424 |                 0 |                 1 |                   0 |                   1 |                   0 |                   1 |                   0 |                   1 |
|  27859 |     12347 |       1065 |       1643 |                      147 |                      364 |                 0 |                 1 |                   0 |                   1 |                   0 |                   1 |                   0 |                   1 |
|  27859 |     12348 |       1065 |       1643 |                      364 |                      364 |                 0 |                 1 |                   0 |                   1 |                   0 |                   1 |                   0 |                   1 |
|  27859 |     12349 |       1065 |       1643 |                      395 |                      395 |                 0 |                 1 |                   0 |                   1 |                   0 |                   1 |                   0 |                   1 |
|  27897 |       179 |       1065 |       1643 |                      366 |                      366 |                 0 |                 0 |                   0 |                   0 |                   0 |                   0 |                   0 |                   0 |
+--------+-----------+------------+------------+--------------------------+--------------------------+-------------------+-------------------+---------------------+---------------------+---------------------+---------------------+---------------------+---------------------+
1 голос
/ 06 августа 2020

Отказ от ответственности: это неполный ответ.

Я могу продолжить позже, но здесь показано, как вычислить острова. Тогда идентификация преступников не должна быть такой сложной.

См. Расширенный пример. Я добавил пользователя 27897 с тремя островами: 0, 1 и 2. См. Ниже:

create table t (UserId int, CompanyID int, WorkPeriodStart date, WorkPeriodEnd date);

insert t (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values
  (27809, 972, '2019-10-10', '2020-10-10'),
  (27853, 484, '2019-10-10', '2020-10-10'),
  (27856, 172, '2019-10-10', '2020-10-10'),
  (27857, 1234, '2015-01-01', '2015-12-31'),
  (27857, 1234, '2016-01-01', '2017-02-28'),
  (27857, 1234, '2017-01-01', '2017-12-31'),
  (27857, 1234, '2018-01-01', '2018-12-31'),
  (27857, 1234, '2019-01-01', '2020-01-31'),
  (27857, 1234, '2020-01-01', '2020-12-31'),
  (27897, 179, '2015-05-28', '2015-09-30'),
  (27897, 179, '2017-03-11', '2017-04-30'),
  (27897, 188, '2017-02-20', '2017-07-07'),
  (27897, 179, '2019-10-10', '2020-10-10');

С этими данными запрос, вычисляющий остров для каждой строки, может выглядеть так:

select *,
  sum(hop) over(partition by UserId order by WorkPeriodStart) as island
from (
  select *,
    case when WorkPeriodStart > dateadd(day, 1, max(WorkPeriodEnd) 
      over(partition by UserId 
           order by WorkPeriodStart 
           rows between unbounded preceding and 1 preceding))
         then 1 else 0 end as hop
  from t
) x
order by UserId, WorkPeriodStart

Результат:

UserId  CompanyID  WorkPeriodStart  WorkPeriodEnd  hop  island
------  ---------  ---------------  -------------  ---  ------
 27809        972  2019-10-10       2020-10-10       0       0
 27853        484  2019-10-10       2020-10-10       0       0
 27856        172  2019-10-10       2020-10-10       0       0
 27857       1234  2015-01-01       2015-12-31       0       0
 27857       1234  2016-01-01       2017-02-28       0       0
 27857       1234  2017-01-01       2017-12-31       0       0
 27857       1234  2018-01-01       2018-12-31       0       0
 27857       1234  2019-01-01       2020-01-31       0       0
 27857       1234  2020-01-01       2020-12-31       0       0
 27897        179  2015-05-28       2015-09-30       0       0
 27897        188  2017-02-20       2017-07-07       1       1
 27897        179  2017-03-11       2017-04-30       0       1
 27897        179  2019-10-10       2020-10-10       1       2

Теперь мы можем расширить этот запрос, чтобы получить «рабочие дни» для каждого острова и «выходные дни» перед каждым островом, выполнив:

select *,
  datediff(day, s, e) + 1 as worked,
  datediff(day, lag(e) over(partition by UserId order by island), s) as prev_days_off
from (
  select UserId, island, min(WorkPeriodStart) as s, max(WorkPeriodEnd) as e
  from (
    select *,
      sum(hop) over(partition by UserId order by WorkPeriodStart) as island
    from (
      select *,
        case when WorkPeriodStart > dateadd(day, 1, max(WorkPeriodEnd) 
          over(partition by UserId 
               order by WorkPeriodStart 
               rows between unbounded preceding and 1 preceding))
             then 1 else 0 end as hop
      from t
    ) x
  ) y
  group by UserId, island
) x
order by UserId, island

Результат:

UserId  island  s           e           worked  prev_days_off
------  ------  ----------  ----------  ------  -------------
 27809       0  2019-10-10  2020-10-10     367         <null>
 27853       0  2019-10-10  2020-10-10     367         <null>
 27856       0  2019-10-10  2020-10-10     367         <null>
 27857       0  2015-01-01  2020-12-31    2192         <null>
 27897       0  2015-05-28  2015-09-30     126         <null>
 27897       1  2017-02-20  2017-07-07     138            509
 27897       2  2019-10-10  2020-10-10     367            825

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

0 голосов
/ 13 августа 2020

Вот что у меня получилось.

Проблемы, с которыми я постоянно сталкивался, были:

  • Как можно Я обрабатываю любые совпадения диапазонов дат и определяю только дни в рамках диапазонов дат контракта.
  • Клиент ЕЩЕ с использованием SQL 2008, поэтому мне нужна старая (э) школа t sql.
  • Убедитесь, что время перерывов (время между контрактами) точно рассчитано.

Поэтому я решил придумать свое собственное решение, которое, вероятно, глупо, поскольку что ему необходимо создавать запись в памяти для каждой комбинации рабочего дня / кандидата. Я не вижу, чтобы таблица контрактов выходила за пределы диапазона записей 5-10 тыс. Единственная причина, по которой я иду в этом направлении.

Я создал календарную таблицу с каждой датой в ней с 01.01.1980 по 31.12.2050, а затем оставил, присоединив диапазоны контрактов к календарной таблице с помощью CandidateId . Это будут отработанные даты. Любые даты в календарной таблице, которые не совпадают с датой в диапазоне контракта, являются днем ​​перерыва.

Календарная таблица

if object_id('CalendarTable') is not null drop table CalendarTable
go

create table CalendarTable (pk int identity, CalendarDate date )

declare @StartDate date = cast('1980-01-01' as date)
declare @EndDate date = cast('2050-12-31' as date)
while @StartDate <= @EndDate
begin
    insert into CalendarTable ( CalendarDate ) values ( @StartDate )
    set @StartDate = dateadd(dd, 1, @StartDate)
end
go

Запрос на 5-летние нарушения (работа 5 лет без 6-месячного периода обдумывания)

declare @enddate date = dateadd(dd, 30, getdate()) 
declare @beginDate date = dateadd(dd, -180, dateadd(year, -5, getdate()))

select poss.CandidateId, 
min(work.CalendarDate) as FirstWorkDate, 
count(work.CandidateId) as workedDays, 
sum(case when work.CandidateId is null then 1 else 0 end) as breakDays, 
case when count(work.CandidateId) > (365*5) and sum(case when work.CandidateId is null then 1 else 0 end) < (365/2) then 1 else 0 end as Year5Violation,
case when count(work.CandidateId) > (365*5) and sum(case when work.CandidateId is null then 1 else 0 end) < (365/2) then DATEADD(year, 5, min(work.CalendarDate)) else null end as ViolationDate
from 
(
    select cand.CandidateId, cal.CalendarDate
    from CalendarTable cal
    join (select distinct c.CandidateId from contracts c where c.WorkPeriodStart is not null and c.WorkPeriodEnd is not null and c.Deleted = 0) cand on 1 = 1
    where cal.CalendarDate between @beginDate and @enddate
) as poss 
left join 
(
    select distinct c.CandidateId, cal.CalendarDate
    from contracts c
        join CalendarTable cal on cal.CalendarDate between c.WorkPeriodStart and c.WorkPeriodEnd 
    where c.WorkPeriodStart is not null and c.WorkPeriodEnd is not null and c.Deleted = 0
) as work on work.CandidateId = poss.CandidateId and work.CalendarDate = poss.CalendarDate
group by poss.CandidateId
...