计算每个员工在一个月内度假的天数SQL Server

时间:2017-05-10 16:46:57

标签: sql sql-server tsql

我有这张桌子:

Vacationtbl

    ID   Start      End
    -------------------------
    01   04/10/17   04/12/17
    01   04/27/17   05/02/17
    02   04/13/17   04/15/17
    02   04/17/17   04/20/17
    03   06/14/17   06/22/17

Employeetbl:

ID   Fname   Lname
------------------
01   John    AAA
02   Jeny    BBB
03   Jeby    CCC

我喜欢计算每位员工4月份休假的天数。

我的查询:

SELECT 
    SUM(DATEDIFF(DAY, Start, End) + 1) AS Days 
FROM 
    Vacationtbl
GROUP BY 
    ID
  • 01返回9(不正确)
  • 02返回7(正确)

如何修复查询,使其计算到月末,并在月末停止。例如,4月有30天。在第二行,员工01应在4/27/17之前计算4/30/1705/02/17适用于五月。

由于

5 个答案:

答案 0 :(得分:3)

Tally / Calendar表是最佳选择。但是,您可以使用 ad-hoc 计数表。

示例

Select Year  = Year(D)
      ,Month = Month(D)
      ,ID
      ,Days  = count(*)
 From  Vacationtbl A
 Cross Apply (
                Select Top (DateDiff(DAY,[Start],[End])+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),[Start]) 
                 From  master..spt_values 
             ) B
 -- YOUR OPTIONAL WHERE STATEMENT HERE --
 Group By ID,Year(D),Month(D)
 Order By 1,2,3

<强>返回

Year    Month   ID  Days
2017    4       01  7
2017    4       02  7
2017    5       01  2
  

编辑 - 即使零日显示所有ID

Select ID
      ,Year  = Year(D)
      ,Month = Month(D)
      ,Days  = sum(case when D between [Start] and [End] then 1 else 0 end)
 From (
       Select Top (DateDiff(DAY,'05/01/2017','05/31/2017')+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),'05/01/2017')  
        From  master..spt_values 
      ) D
 Cross Join Vacationtbl  B
 Group By ID,Year(D),Month(D)
 Order By 1,2,3

<强>返回

ID  Year    Month   Days
1   2017    5       2
2   2017    5       0

<强> dbFiddle if it Helps

  

编辑 - 2纠正重叠(差距和群岛)

--Create Some Sample Data
----------------------------------------------------------------------
Declare @Vacationtbl Table ([ID] varchar(50),[Start] date,[End] date)
Insert Into @Vacationtbl Values
 (01,'04/10/17','04/12/17')
,(01,'04/27/17','05/02/17')
,(02,'04/13/17','04/15/17')
,(02,'04/17/17','04/20/17')
,(02,'04/16/17','04/17/17')  -- << Overlap
,(03,'05/16/17','05/17/17')

-- The Actual Query
----------------------------------------------------------------------
Select ID
      ,Year  = Year(D)
      ,Month = Month(D)
      ,Days  = sum(case when D between [Start] and [End] then 1 else 0 end)
 From (Select Top (DateDiff(DAY,'04/01/2017','04/30/2017')+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),'04/01/2017')  From  master..spt_values ) D
 Cross Join (
                Select ID,[Start] = min(D),[End] = max(D)
                 From (
                        Select E.*,Grp = Dense_Rank() over (Order By D) - Row_Number() over (Partition By ID Order By D)
                         From (
                                Select Distinct A.ID,D
                                  From  @Vacationtbl A
                                  Cross Apply (Select Top (DateDiff(DAY,A.[Start],A.[End])+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),A.[Start]) From  master..spt_values ) B
                               ) E
                      ) G
                 Group By ID,Grp    
            )  B
 Group By ID,Year(D),Month(D)
 Order By 1,2,3

<强>返回

ID  Year    Month   Days
1   2017    4       7
2   2017    4       8
3   2017    4       0

答案 1 :(得分:1)

没有日期表,您可以使用

select Id
     ,sum(case when [end]>'20170430' and [start]<'20170401' then datediff(day,'20170401','20170430')+1
               when [end]>'20170430' then datediff(day,[start],'20170430')+1
               when [start]<'20170401' then datediff(day,'20170401',[end])+1
          else datediff(day,[start],[end])+1
        end) as VacationDays
from Vacationtbl
where [start] <= '20170430' and [end] >= '20170401'
group by Id

这里有3个条件

  • 开始时间是本月之前,结束时间是本月之后。在这种情况下,您将减去该月的结束日期和开始日期。
  • 结束是月末,开始是月,在这种情况下从月末减去月结束日期。
  • 开始时间是本月之前,但结束时间是月份。在这种情况下,减去月份开始日期和结束日期。

编辑:根据OP的评论,未来的日期必须包括在内,

/*This recursive cte generates the month start and end dates with in a given time frame
For Eg: all the month start and end dates for 2017
Change the start and end period as needed*/
with dates (month_start_date,month_end_date) as 
(select cast('2017-01-01' as date),cast(eomonth('2017-01-01') as date)
 union all
 select dateadd(month,1,month_start_date),eomonth(dateadd(month,1,month_start_date))  from dates
 where month_start_date < '2017-12-01'
)
--End recursive cte
--Query logic is the same as above
select v.Id
,year(d.month_start_date) as yr,month(d.month_start_date) as mth
,sum(case when v.[end]>d.month_end_date and v.[start]<d.month_start_date then datediff(day,d.month_start_date,d.month_end_date)+1
          when v.[end]>d.month_end_date then datediff(day,v.[start],d.month_end_date)+1
          when v.[start]<d.month_start_date then datediff(day,d.month_start_date,v.[end])+1
     else datediff(day,v.[start],v.[end])+1
     end) as VacationDays
from dates d
join Vacationtbl v on v.[start] <= d.month_end_date and v.[end] >= d.month_start_date
group by v.id,year(d.month_start_date),month(d.month_start_date)

答案 2 :(得分:0)

您可以使用日历或日期表进行此类操作。

对于内存中只有152kb,您可以在表格中拥有30年的日期:

/* dates table */
declare @fromdate date = '20000101';
declare @years    int  = 30;
/* 30 years, 19 used data pages ~152kb in memory, ~264kb on disk */
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
select top (datediff(day, @fromdate,dateadd(year,@years,@fromdate)))
    [Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,@fromdate))
into dbo.Dates
from n as deka cross join n as hecto cross join n as kilo
               cross join n as tenK cross join n as hundredK
order by [Date];
create unique clustered index ix_dbo_Dates_date
  on dbo.Dates([Date]);

如果不采取创建表格的实际步骤,您可以在common table expression内使用它:

declare @fromdate date = '20170401';
declare @thrudate date = '20170430';
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
  select top (datediff(day, @fromdate, @thrudate)+1) 
      [Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,@fromdate))
  from n as deka cross join n as hecto cross join n as kilo
                cross join n as tenK cross join n as hundredK
   order by [Date]
)
select [Date]
from dates;

使用如下:

select 
    v.Id
  , count(*) as VacationDays
from Vacationtbl v
  inner join Dates d
    on d.Date >= v.[Start]
   and d.Date <= v.[End]
where d.Date >= '20170401'
  and d.Date <= '20170430'
group by v.Id

rextester demo(表格):http://rextester.com/PLW73242

rextester demo(cte):http://rextester.com/BCY62752

返回:

+----+--------------+
| Id | VacationDays |
+----+--------------+
| 01 |            7 |
| 02 |            7 |
+----+--------------+

数字和日历表参考:

答案 3 :(得分:0)

假设您只想要一个月并且想要计算所有日期,您可以使用算术来完成此操作。不需要单独的日历表。优势在于性能。

我认为如果SQL Server支持least()greatest()会更容易,但case会这样做:

select id,
       sum(1 + datediff(day, news, newe)) as vacation_days_april
from vactiontbl v cross apply
     (values (case when [start] < '2017-04-01' then cast('2017-04-01' as date) else [start] end),
             (case when [end] >= '2017-05-01' then cast('2017-04-30' as date) else [end] end)
     ) v(news, newe)
where news <= newe
group by id;

您可以随时将其扩展到任何月份:

with m as (
      select cast('2017-04-01' as date) as month_start, 
             cast('2017-04-30' as date) as month_end
     )

select id,
       sum(1 + datediff(day, news, newe)) as vacation_days_aprile
from m cross join
     vactiontbl v cross apply
     (values (case when [start] < m.month_start then m.month_start else [start] end),
             (case when [end] >= m.month_end then m.month_end else [end] end)
     ) v(news, newe)
where news <= newe
group by id;

您甚至可以使用类似的想法延长到多个月,每个用户和每个月都有不同的行。

答案 4 :(得分:0)

试试这个,

declare @Vacationtbl table(ID int,Startdate date,Enddate date)
 insert into @Vacationtbl VALUES   
(1   ,'04/10/17','04/12/17')
,(1   ,'04/27/17','05/02/17')
,(2   ,'04/13/17','04/15/17')
,(2   ,'04/17/17','04/20/17')
-- somehow convert your input into first day of month
Declare @firstDayofGivenMonth date='2017-04-01'
Declare @LasttDayofGivenMonth date=dateadd(day,-1,dateadd(month,datediff(month,0,@firstDayofGivenMonth)+1,0))

;with CTE as
(
select * 
,case when Startdate<@firstDayofGivenMonth then @firstDayofGivenMonth else Startdate end NewStDT
,case when Enddate>@LasttDayofGivenMonth then @LasttDayofGivenMonth else Enddate end NewEDT
from @Vacationtbl
)

SELECT 
SUM(DATEDIFF(DAY, NewStDT, NewEDT) + 1) AS Days 
FROM 
CTE
GROUP BY 
ID