根据连续日期查找最近的日期

时间:2014-11-11 14:35:06

标签: sql sql-server greatest-n-per-group

我的桌子上列出了所有员工的缺勤(假期),我们想知道的是今天谁离开,以及他们将返回的日期。

不幸的是,没有给出缺席ID,所以如果其中一个日期是今天,你不能只从缺席ID中检索最大日期。

然而,缺少,因为它们是输入,因此每天给出一个递增ID,所以如果有一个带有今天日期的条目,我需要一个查找employeeID的查询,然后将AbsenceID列增加到找到缺席的最长日期。

表格示例(假设今天的日期是2014年11月11日,英国格式):

AbsenceID   EmployeeID    AbsenceDate
100         10            11/11/2014
101         10            12/11/2014
102         10            13/11/2014
103         10            14/11/2014
104         10            15/11/2014
107         21            11/11/2014
108         21            12/11/2014
120         05            11/11/2014
130         15            20/11/2014
140         10            01/03/2015
141         10            02/03/2015
142         10            03/03/2015
143         10            04/03/2015

因此,从上述情况来看,我们希望返回日期为:

EmployeeID     ReturnDate
10             15/11/2014
21             12/11/2014
05             11/11/2014

编辑:请注意,140-143范围不能包含在未来出现的结果中,并且今天没有任何日期范围。

据推测,我需要在每个条目上运行一个迭代子函数,其中包含employeeID匹配的今天日期。

3 个答案:

答案 0 :(得分:1)

因此,基于我认为您所要求的内容,您希望根据您在系统中记录的假期返回当前关闭的人员以及预计何时返回的人员列表,这应该仅适用于连续几天。

SQL Fiddle Demo

架构设置

CREATE TABLE EmployeeAbsence
    ([AbsenceID] int, [EmployeeID] int, [AbsenceDate] DATETIME)
;

INSERT INTO EmployeeAbsence
    ([AbsenceID], [EmployeeID], [AbsenceDate])
VALUES
    (100, 10, '2014-11-11'),
    (101, 10, '2014-11-12'),
    (102, 10, '2014-11-13'),
    (103, 10, '2014-11-14'),
    (104, 10, '2014-11-15'),
    (107, 21, '2014-11-11'),
    (108, 21, '2014-11-12'),
    (120, 05, '2014-11-11'),
    (130, 15, '2014-11-20')
;

用于生成输出的递归CTE

;WITH cte AS (
    SELECT EmployeeID, AbsenceDate
    FROM dbo.EmployeeAbsence
    WHERE AbsenceDate = CAST(GETDATE() AS DATE)
    UNION ALL
    SELECT  e.EmployeeID, e.AbsenceDate
    FROM cte
    INNER JOIN dbo.EmployeeAbsence e ON e.EmployeeID = cte.EmployeeID 
           AND e.AbsenceDate = DATEADD(d,1,cte.AbsenceDate)
    )
SELECT cte.EmployeeID, MAX(cte.AbsenceDate) 
FROM cte
GROUP BY cte.EmployeeID

<强> Results

| EMPLOYEEID |                     Return Date |
|------------|---------------------------------|
|          5 | November, 11 2014 00:00:00+0000 |
|         10 | November, 15 2014 00:00:00+0000 |
|         21 | November, 12 2014 00:00:00+0000 |

<强>解释

CTE中的第一个SELECT通过此过滤器获取今天关闭的员工:

WHERE AbsenceDate = CAST(GETDATE() AS DATE)

然后将此结果集UNIONED返回到EmployeeAbsence表,其中的联接与EmployeeID以及AbsenceDate + 1天匹配,以递归方式查找连续日期:

-- add a day to the cte.AbsenceDate from the first SELECT
e.AbsenceDate = DATEADD(d,1,cte.AbsenceDate) 

最终SELECT只是将员工的cte结果与每位员工计算的MAX AbsenceDate进行分组。

SELECT cte.EmployeeID, MAX(cte.AbsenceDate) 
FROM cte
GROUP BY cte.EmployeeID

不包括周末:

我已根据您的评论进行了快速测试,如果CTE检测到添加一天将导致星期六,则在添加额外天数时,CTE中INNER JOIN的以下修改应排除周末: / p>

INNER JOIN dbo.EmployeeAbsence e ON e.EmployeeID = cte.EmployeeID 
       AND e.AbsenceDate = CASE WHEN datepart(dw,DATEADD(d,1,cte.AbsenceDate)) = 7 
                                THEN DATEADD(d,3,cte.AbsenceDate) 
                           ELSE DATEADD(d,1,cte.AbsenceDate) END

因此,当您添加一天时:datepart(dw,DATEADD(d,1,cte.AbsenceDate)) = 7,如果结果是星期六(7),那么您需要添加3天而不是1来获取星期一:DATEADD(d,3,cte.AbsenceDate)

答案 1 :(得分:0)

您需要做一些事情才能将这些数据转换为可用的格式。您需要能够找出小组的开始和结束位置。这个例子很难,因为没有直接的分组列。

因此,我们可以计算组开始和结束的时间,您需要创建一个包含所有列的CTE,并使用LAG()来获取前一个AbsenceIDEmployeeID每行的行。在此CTE中,您还应同时使用ROW_NUMBER(),以便我们可以再次将行重新排序为相同的顺序。

类似的东西:

WITH
    [AbsenceStage] AS (
        SELECT [AbsenceID], [EmployeeID], [AbsenceDate]
            ,[RN] = ROW_NUMBER() OVER (ORDER BY [EmployeeID] ASC, [AbsenceDate] ASC, [AbsenceID] ASC)
            ,[AbsenceID_Prev] = LAG([AbsenceID]) OVER (ORDER BY [EmployeeID] ASC, [AbsenceDate] ASC, [AbsenceID] ASC)
            ,[EmployeeID_Prev] = LAG([EmployeeID]) OVER (ORDER BY [EmployeeID] ASC, [AbsenceDate] ASC, [AbsenceID] ASC)
        FROM [HR_Absence]
    )

现在我们有了这个,我们可以将每一行与前一行进行比较,以查看当前行是否在另一个&#34;组&#34;到上一行。

条件如下:

   [EmployeeID_Prev] IS NULL -- We have a new group if the previous row is null
OR [EmployeeID_Prev] <> [EmployeeID] -- Or if the previous row is for a different employee
OR [AbsenceID_Prev] <> ([AbsenceID]-1) -- Or if the AbsenceID is not sequential

然后,您可以使用此功能将CTE加入其自身,以便在每个组中找到第一行,例如:

....
FROM [AbsenceStage] AS [Row]
INNER JOIN [AbsenceStage] AS [First]
    ON ([First].[RN] = (
        -- Get the first row before ([RN] Less that or equal to) this one where it is the start of a grouping
        SELECT MAX([RN]) FROM [AbsenceStage]
        WHERE [RN] <= [Row].[RN] AND (
               [EmployeeID_Prev] IS NULL
            OR [EmployeeID_Prev] <> [EmployeeID]
            OR [AbsenceID_Prev] <> ([AbsenceID]-1)
        )
    ))
...

然后,GROUP BY [First].[RN] SELECT [Row].[EmployeeID] ,MIN([Row].[AbsenceDate]) AS [Absence_Begin] ,MAX([Row].[AbsenceDate]) AS [Absence_End] ... -- FROM and INNER JOIN from above ... GROUP BY [First].[RN], [Row].[EmployeeID]; 现在可以充当群组ID,并允许您获取每个缺席群组的开始和结束日期。

EmployeeID

然后,您可以将所有内容放入视图中,为WHERE CAST(CURRENT_TIMESTAMP AS date) BETWEEN [Absence_Begin] AND [Absence_End] 提供每次缺席的开始日期和结束日期。然后,您可以通过以下方式轻松拉出当前关闭的员工:

{{1}}

<强> SQL Fiddle

答案 2 :(得分:0)

像这里的另一个答案一样,我将创建假期间隔,但是通过不同的方法。首先是代码:

declare @today date = getdate(); --use whatever date here
with g as (
    select *, dateadd(day, -1 * row_number() over (partition by employeeid order by absencedate), AbsenceDate) as group_number
    from employeeabsence
) , leave_intervals as (
    select employeeid, min(absencedate) as [start], max(absencedate) as [end]
    from g
    group by EmployeeID, group_number
)
select employeeid, [start], [end]
from leave_intervals
where @today between [start] and [end]

作为解释,我们首先将日期值放入变量中。我今天选择了,但是这段代码适用于传入的任何日期。接下来,我们创建一个公用表表达式(CTE),它将在表的分组列上添加。这是解决方案的核心,因此需要一些处理。在给定的时间间隔内,AbsenceDate以每行一天的速度增加。 row_number()也以每行一个的速度增加。因此,如果我们从AbsenceDate中减去row_number()天数,我们将获得另一个(任意)日期。这里的关键是要意识到该任意日期对于区间中的每一行都是相同的,因此我们可以使用它来分组。从那里开始,这只是做到这一点的问题;获得每个间隔的最小值和最大值。最后,我们发现间隔包含@today。