【问题标题】:Finding Start and End Dates from Date Numbers Table (Date Durations)从日期编号表(日期持续时间)中查找开始和结束日期
【发布时间】:2026-01-31 14:45:01
【问题描述】:

我有两个表:一个包含有关如何安排员工的信息的日程表和一个数字表,其中每个数字对应一个日期。

表格如下:

[Employee Schedule]

ID          Employee ID Project ID  Day ID
----------- ----------- ----------- -----------
1           64          2           168
2           64          2           169
3           64          2           170
4           64          2           171
5           64          1           169
6           64          1           170
7           64          1           171
8           64          1           172
9           64          2           182
10          64          2           183
11          64          2           184

[Day Numbers]

ID          Day
----------- ----------
168         2009-06-18
169         2009-06-19
170         2009-06-20
171         2009-06-21
172         2009-06-22
173         2009-06-23
174         2009-06-24
175         2009-06-25
176         2009-06-26
177         2009-06-27
178         2009-06-28
179         2009-06-29
180         2009-06-30
181         2009-07-01
182         2009-07-02
183         2009-07-03
184         2009-07-04

如您所见,员工 64 计划在 2009 年 6 月 19 日至 2009 年 6 月 22 日的项目 1 和 2009 年 6 月 18 日至 2009 年 6 月 21 日和 2009 年 7 月 2 日的项目 2至 2009 年 7 月 4 日。

我的问题是:我可以使用什么算法来快速确定员工日程安排的跨度,以便我可以如下显示?

 Employee ID Project ID Duration
 ----------- ---------- ------------
 64          1          2009-06-19 to 2009-06-22
 64          2          2009-06-18 to 2009-06-21
 64          2          2009-07-02 to 2009-07-04

我可以在 SQL 端或代码端执行此操作。如果需要,我可以使用 Linq。该表不需要通过 SQL 编译。这将在网站上动态发生,并且应该尽可能高效。如果不需要的话,我不想遍历每一个并在连续的日子里寻找休息时间。

【问题讨论】:

  • 你可以用分析函数来做这些事情,但我不知道 linq 是否支持这些。
  • 我们可以假设 [Employee Schedule] 中的连续 ID 构成一个范围吗?还是只有 [Day ID]?
  • 我们可以假设 [Employee ID]、[Project ID] 和 [Day ID] 的连续元组构成一个范围。
  • 即:(64,1,169),(64,1,170),(64,1,171) 是该项目和员工的 3 天范围。
  • 还有另一种(可能像泥巴一样慢)解决方案,带有层次查询(从 ... 连接开始)linq 和您的数据库是否支持这些?

标签: sql vb.net linq algorithm clr


【解决方案1】:

假设对于部分解决方案,Day ID 始终是连续的...

select *
  from employee_schedule a                    
 where not exists( select *                          
                     from employee_schedule b        
                    where a.employeeid = b.employeeid
                      and a.projectid  = b.projectid 
                      and (a.dayid - 1) = b.dayid )

列出开始日 ID:

 ID      EMPLOYEEID       PROJECTID           DAYID 
 1              64               2             168 
 5              64               1             169 
 9              64               2             182 



select *
  from employee_schedule a                   
 where not exists( select *                         
                     from employee_schedule b       
                    where a.employeeid = b.employeei
                      and a.projectid  = b.projectid
                      and (a.dayid + 1) = b.dayid )

列出结束日 ID:

  ID      EMPLOYEEID       PROJECTID           DAYID 
  4              64               2             171 
  8              64               1             172 
 11              64               2             184 

【讨论】:

  • 即使它们不是连续的,也可以使用带有 DATEADD 函数的实际日期和相同的比较来重写查询
【解决方案2】:

让我们创建一个视图让事情变得更简单:

create view EmployeeProjectDates
as
select
    e.[Employee ID], e.[Project ID], d.Day
from
    [Employee Scchedule] e
    join [Day Numbers] d on e.[Day Id] = d.ID

您可以执行这样的查询来获取所有开始日期:

select
    one.[Employee ID], one.[Project ID], one.Day as StartDate
from
    EmployeeProjectDays one
    left join EmployeeProjectDays two on one.[Employee ID] = two.[Employee ID] and one.[Project ID] = two.[Project ID] and one.Day = DATEADD(DAY, 1, two.Day)
where
    two.Day is null

然后执行类似的查询以获取结束日期并匹配它们。我认为这样的事情会让你们俩都受益。

select
    one.[Employee ID], one.[Project ID], one.Day as StartDate,
    (select
        min(two_end.Day)
    from
        EmployeeProjectDays one_end
        join EmployeeProjectDays two_end on one_end.[Employee ID] = two_end.[Employee ID] and one_end.[Project ID] = two_end.[Project ID] and one.Day = DATEADD(DAY, 1, two.Day)
     where
        one_end.Day is null
        and two_end.Day > one.Day) as EndDate
from
    EmployeeProjectDays one
    left join EmployeeProjectDays two on one.[Employee ID] = two.[Employee ID] and one.[Project ID] = two.[Project ID] and one.Day = DATEADD(DAY, 1, two.Day)
where
    two.Day is null

我没有测试任何这些查询,但类似的东西应该可以工作。在我们在应用程序代码中实现某些内容之前,我必须使用类似的查询来查找开始和结束日期。

【讨论】:

  • 我试过你的代码,我得到所有结束日期的空值。我不太了解结束日期嵌套连接,无法对其进行调试。
  • 结束日期的子查询与开始日期的查询基本相同,只是颠倒了。它应该为您提供所有结束日期的列表,但对于每一行,您只需要开始日期之后的第一个结束日期。子查询可能还需要确保员工 ID 和项目 ID 与主查询中的值匹配,我忽略了。
【解决方案3】:

这个适用于 oracle,从它开始,它应该也可以在 SQL Server 中使用。 (包括测试脚本)

create table schedule (id number, employee_id number, project_id number, day_id number);

insert into schedule (id, employee_id, project_id, day_id)
values(1,64,2,168);
insert into schedule (id, employee_id, project_id, day_id)
values(2,64,2,169);
insert into schedule (id, employee_id, project_id, day_id)
values(3,64,2,170);
insert into schedule (id, employee_id, project_id, day_id)
values(4,64,2,171);
insert into schedule (id, employee_id, project_id, day_id)
values(5,64,1,169);
insert into schedule (id, employee_id, project_id, day_id)
values(6,64,1,170);
insert into schedule (id, employee_id, project_id, day_id)
values(7,64,1,171);
insert into schedule (id, employee_id, project_id, day_id)
values(8,64,1,172);
insert into schedule (id, employee_id, project_id, day_id)
values(9,64,2,182);
insert into schedule (id, employee_id, project_id, day_id)
values(10,64,2,183);
insert into schedule (id, employee_id, project_id, day_id)
values(11,64,2,184);
insert into schedule (id, employee_id, project_id, day_id)
values(11,65,3,184);

select * 
FROM (
    select  
        employee_id,
        project_id,
        first_day,
        nvl(last_day, 
            lead(last_day) over (
                partition by employee_id, project_id 
                order by nvl(first_day, last_day)
            )
        ) last_day
    from (
        select -- this identifies start and end rows of an interval
            employee_id,
            project_id,
            decode (day_id - prev_day, 1, null, day_id) first_day, -- uses day_id, if prev_day is not really the previous day, i.e. a gap or null
            decode (day_id - next_day, -1, null, day_id) last_day
        from (
            select -- this select adds columns for the previous and next day, in order to identify the boundaries of intervals 
                employee_id, 
                project_id, 
                day_id, 
                lead(day_id) over ( 
                    partition by employee_id, project_id 
                    order by day_id
                ) next_day,
                lag(day_id) over ( 
                    partition by employee_id, project_id 
                    order by day_id
                ) prev_day
            from schedule
        )
    )
    where first_day is not null 
    or last_day is not null-- just filter the rows, that represent start or end dates
) 
where first_day is not null

产生这个输出:

64  1   169 172
64  2   168 171
64  2   182 184
65  3   184 184

【讨论】:

  • 这是一个很好的解决方案。如果 T-SQL 中只有滞后和超前函数。
【解决方案4】:

我没有测试过,但是试试:

select [Employee ID], [Project ID], start + ' to ' + end
from (
    select s.[Employee ID], s.[Project ID], min(d.Day) start, max(d.Day) end
    from [Employee Schedule] s
    inner join [Day Numbers] d on s.[Day ID] = d.[Day ID]
    group by s.[Employee ID], s.[Project ID]
) a

编辑:更正了一些列名

为了更方便查询,我建议您将架构重构为:

[EmployeeSchedule]

ID 
EmployeeID 
ProjectID  
StartDate 
EndDate

并彻底摆脱日数。这将使您的查询更简单、更高效,并且如果您愿意,您可以使用 NULL StartDates 或 EndDates 的记录。

【讨论】:

  • 这是否处理 projectid = 2 的 2 个范围?
  • 那行不通。最小值和最大值之间可能存在不应包含在持续时间中的间隙。
  • 年底会很有趣!
  • @n8wrl - 幸运的是有一个数字表,所以没关系。 :)
  • 没错,它不会处理同一员工/项目的多个条目。我可以建议您重构架构以在 [Employee Schedule] 表中包含 StartDay 和 EndDay 列吗?可以编写查询以使用现有架构,但与更改架构相比,它的工作量更大,效率更低。
最近更新 更多