2017-04-16 49 views
2

我們正在構建一個使用SQL Server作爲後端數據庫(使用ASP.NET作爲Web API)的簡單調度平臺。SQL Server:自動生成過濾範圍內的記錄?

在數據庫中的相關表如下:

CREATE TABLE Employee 
(
    EmpID INT NOT NULL,  -- identity column 
    FirstName VARCHAR(64), 
    LastName VARCHAR(64), 
    ...      -- other columns identifying the employee 
    IsActive BIT NOT NULL -- false if employee shouldn't show as available 
) 

CREATE TABLE Shifts 
(
    ShiftID INT NOT NULL, -- identity column 
    EmpID INT NOT NULL,  -- foreign key 
    startTime TIME NOT NULL, -- only the time of day 
    endTime TIME NOT NULL, 
    expiryDate DATE,   -- no availability after this day 
    weekdayMask INT NOT NULL -- 0x01 = Monday, 0x04 = Wednesday, etc. ORed together 
) 

CREATE TABLE Appointment 
(
    ApptID INT NOT NULL,  -- identity column 
    EmpID INT NOT NULL,  -- foreign key 
    apptDate DATE   -- day appointment takes place 
    startTime TIME NOT NULL, -- only the time of day 
    endTime TIME NOT NULL, 
) 

我現在想寫一個表函數(或存儲過程可以做到這一點),將獲得的可用性的日曆給定員工,給定員工的ID和日期。我需要考慮以下幾點:

  • 假設所有約會都發生在離散的半小時塊中。預約可以是多個連續的塊。我們圍繞下一個塊,所以如果約會在1:33 PM結束,那麼1:30-2:00PM塊仍然應該被視爲佔用。

  • 僱員必須存在用於整個塊是可用的。如果員工說他們的班次在下午4:45結束,那麼4:30應該不是有效的塊開始時間。

  • 僱員可以每天,例如多個班次上午9點至下午12點,下午1點至下午5點。

  • (簡單部分)該功能對於不可用的員工應該什麼也不顯示,並且如果員工的expiryDate已經通過,應該不會顯示任何內容。

  • 該函數所需的返回值是半小時塊的列表員工在期間可用,按開始塊時間列出。

示例:假設員工1的班次從上午9點到下午3點。員工1的預約時間從上午10點到上午11點,並且從下午1點30分到下午2點有一個預約。從功能所需的回報會是這樣一個列表:

AvailableBlockStartingTime 
-------------------------- 
9:00 AM 
9:30 AM 
11:00 AM 
11:30 AM 
12:00 PM 
12:30 PM 
1:00 PM 
2:00 PM 
2:30 PM 

所以基本上,我需要有條件地在給定的時間間隔產生的記錄 - 如果預約記錄存在但跳過的記錄 - 在這種情況下,一個半小時給定的時間塊。

我已經能夠做的「天真」事情是測試給定時間,並查看當時是否有員工可用於預約,例如,給出「今天」和「上午11點30分」,我可以確定員工是否可用。

所以我需要自動在半小時內重複上述步驟,並將所有結果添加到我可以返回的「表」中,或者使用某種SQL魔術來生成記錄的「範圍」然後排除這些範圍內的事物。

想法?

回答

2

生成一組時間範圍的問題可以通過即席查詢,時間表來解決。

declare @starttime time(0) = '09:00'; 
declare @endtime time(0) = '15:00'; 
with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n)) 
/* adhoc time range table */ 
, blocks as (
    select top (48) 
     [block]=convert(time(0),dateadd(minute,30*(row_number() over (order by (select 1)) -1),0)) 
    , [next_block]=convert(time(0),dateadd(minute,30*(row_number() over (order by (select 1))),0)) 
    from n as deka cross join n as hecto 
    order by row_number() over (order by (select 1)) 
) 
select * 
from blocks b 
where b.block >= @starttime 
    and b.block < @endtime; 

rextester演示:http://rextester.com/WJZY39264

回報:

+----------+------------+ 
| block | next_block | 
+----------+------------+ 
| 09:00:00 | 09:30:00 | 
| 09:30:00 | 10:00:00 | 
| 10:00:00 | 10:30:00 | 
| 10:30:00 | 11:00:00 | 
| 11:00:00 | 11:30:00 | 
| 11:30:00 | 12:00:00 | 
| 12:00:00 | 12:30:00 | 
| 12:30:00 | 13:00:00 | 
| 13:00:00 | 13:30:00 | 
| 13:30:00 | 14:00:00 | 
| 14:00:00 | 14:30:00 | 
| 14:30:00 | 15:00:00 | 
+----------+------------+ 

解碼你的weekdayMask如果你依賴於星期一是一週的第一天,使用datepart(weekday,...)可以得到脆(這根據語言設置而改變,並且被set datefirst覆蓋)。

我的解決辦法是,檢查weekdaydatename()和從表中拉關聯的值,(這裏我在一個共同的表表達式中使用的表值構造來代替。)

其他條件而定可以應用使用聯接,whereisnull()coalesce()處理null值(expiryDate),子查詢得到一個工作日來比較weekdayMask,並not exists()檢查重疊的相關值。

最後一個問題是是否使用過程或表值函數。如果您打算使用表值函數,我強烈建議使用內聯表值函數來獲得顯着的性能改進,而不是使用可比較的多語句表值函數。由於這似乎比一個程序更難處理,所以我選擇了這個答案。

如果你決定你喜歡什麼,重寫一個表值函數作爲一個過程應該很容易。

create function dbo.udf_availability_by_empId_Date (
    @empid int 
    , @date date 
) returns table with schemabinding as return (
/* Weekday mask using weekday names 
    to avoid any issue with datefirst and datepart() 
    this uses a 7 row lookup table for the bitmask check */ 
with Mask as (
    select weekday_name, weekday_value 
    from (values ('Monday',1) ,('Tuesday',2) ,('Wednesday',4) ,('Thursday',8) 
,('Friday',16) ,('Saturday',32) ,('Sunday',64)) w (weekday_name,weekday_value)) 
/* adhoc numbers table to generate range in half hour increments */ 
, n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n)) 
/* adhoc time range table */ 
, blocks as (
    select top (48) 
     [block]=convert(time(0),dateadd(minute,30*(row_number() over (order by (select 1)) -1),0)) 
    , [next_block]=convert(time(0),dateadd(minute,30*(row_number() over (order by (select 1))),0)) 
    from n as deka cross join n as hecto 
    order by row_number() over (order by (select 1)) 
) 
select b.block 
from dbo.employee e 
    inner join dbo.shifts s 
    on s.EmpId = e.Empid 
    inner join blocks b 
    on b.block >= s.starttime 
    and b.block < s.endtime 
where e.EmpId = @empid 
    and e.IsActive = 1 
    and isnull(s.ExpiryDate,'20630405') >= @date 
    /* get the value to check against the weekday bitmask */ 
    and s.weekdaymask & (
    select weekday_value 
    from Mask 
    where weekday_name = datename(weekday,@date) 
    ) != 0 
    /* check for appointments that overlap the block */ 
    and not exists (
    select 1 
    from dbo.appointment a 
    where a.endtime > b.block 
     and b.next_block > a.starttime 
     and a.apptDate = @date 
     and a.Empid = @empid 
    ) 
); 
go 

如果你想改變返回time的顯示器,那麼你可以使用convert(varchar(10),a.block,100)得到一個AM/PM格式的字符串,但我會建議讓你的應用層做了格式化。

爲了您的測試用例:

select 
    a.block 
    , AvailableBlockStartingTime = convert(varchar(10),a.block,100) 
from dbo.udf_availability_by_empId_Date(1,'20170416') as a 

rextester演示:http://rextester.com/TXBUP51531

回報:

+----------+----------------------------+ 
| block | AvailableBlockStartingTime | 
+----------+----------------------------+ 
| 09:00:00 | 9:00AM      | 
| 09:30:00 | 9:30AM      | 
| 11:00:00 | 11:00AM     | 
| 11:30:00 | 11:30AM     | 
| 12:00:00 | 12:00PM     | 
| 12:30:00 | 12:30PM     | 
| 13:00:00 | 1:00PM      | 
| 14:00:00 | 2:00PM      | 
| 14:30:00 | 2:30PM      | 
+----------+----------------------------+ 

因爲它是一個表值函數,我們不妨有apply()一些有趣。

/* show all employee availability for a given date */ 
select 
    e.EmpId 
    , e.FirstName 
    , e.IsActive 
    , a.block 
from dbo.Employee e 
    outer apply dbo.udf_availability_by_empId_Date(e.EmpId,'20170416') a 

回報:

+-------+----------------------+----------+----------+ 
| EmpId |  FirstName  | IsActive | block | 
+-------+----------------------+----------+----------+ 
|  1 | fdmillion   | True  | 09:00:00 | 
|  1 | fdmillion   | True  | 09:30:00 | 
|  1 | fdmillion   | True  | 11:00:00 | 
|  1 | fdmillion   | True  | 11:30:00 | 
|  1 | fdmillion   | True  | 12:00:00 | 
|  1 | fdmillion   | True  | 12:30:00 | 
|  1 | fdmillion   | True  | 13:00:00 | 
|  1 | fdmillion   | True  | 14:00:00 | 
|  1 | fdmillion   | True  | 14:30:00 | 
|  2 | shift expired person | True  | NULL  | 
|  3 | inactive person  | False | NULL  | 
|  4 | other person   | True  | NULL  | 
+-------+----------------------+----------+----------+ 

或檢查的時間表一週僱員:

/* show a weeks worth of availability for a given empid */ 
declare @empid int = 1 
declare @fromdate date = '20170410'; 
;with n as (select [Date]=dateadd(day,n,@fromdate) from (values(0),(1),(2),(3),(4),(5),(6)) t(n)) 
select 
    [Date] = convert(char(10),n.[Date],120) 
    , Weekday_Name = datename(weekday,n.[Date]) 
    , a.block 
from n 
    outer apply dbo.udf_availability_by_empId_Date(@empid,[Date]) a 
order by n.[Date] 

回報:

+------------+--------------+----------+ 
| Date | Weekday_Name | block | 
+------------+--------------+----------+ 
| 2017-04-10 | Monday  | NULL  | 
| 2017-04-11 | Tuesday  | NULL  | 
| 2017-04-12 | Wednesday | NULL  | 
| 2017-04-13 | Thursday  | NULL  | 
| 2017-04-14 | Friday  | 09:00:00 | 
| 2017-04-14 | Friday  | 09:30:00 | 
| 2017-04-14 | Friday  | 10:00:00 | 
| 2017-04-14 | Friday  | 10:30:00 | 
| 2017-04-14 | Friday  | 11:00:00 | 
| 2017-04-14 | Friday  | 11:30:00 | 
| 2017-04-14 | Friday  | 12:00:00 | 
| 2017-04-14 | Friday  | 12:30:00 | 
| 2017-04-14 | Friday  | 13:00:00 | 
| 2017-04-14 | Friday  | 13:30:00 | 
| 2017-04-14 | Friday  | 14:00:00 | 
| 2017-04-14 | Friday  | 14:30:00 | 
| 2017-04-15 | Saturday  | 09:00:00 | 
| 2017-04-15 | Saturday  | 09:30:00 | 
| 2017-04-15 | Saturday  | 10:00:00 | 
| 2017-04-15 | Saturday  | 10:30:00 | 
| 2017-04-15 | Saturday  | 11:00:00 | 
| 2017-04-15 | Saturday  | 11:30:00 | 
| 2017-04-15 | Saturday  | 12:00:00 | 
| 2017-04-15 | Saturday  | 12:30:00 | 
| 2017-04-15 | Saturday  | 13:00:00 | 
| 2017-04-15 | Saturday  | 13:30:00 | 
| 2017-04-15 | Saturday  | 14:00:00 | 
| 2017-04-15 | Saturday  | 14:30:00 | 
| 2017-04-16 | Sunday  | 09:00:00 | 
| 2017-04-16 | Sunday  | 09:30:00 | 
| 2017-04-16 | Sunday  | 11:00:00 | 
| 2017-04-16 | Sunday  | 11:30:00 | 
| 2017-04-16 | Sunday  | 12:00:00 | 
| 2017-04-16 | Sunday  | 12:30:00 | 
| 2017-04-16 | Sunday  | 13:00:00 | 
| 2017-04-16 | Sunday  | 14:00:00 | 
| 2017-04-16 | Sunday  | 14:30:00 | 
+------------+--------------+----------+ 

附加說明:time(0)對於time(缺省分數精度爲7)是3個字節對5個字節。您可以通過包含0-2的精度來削減每行的4個字節。

參考:

+0

非常全面。我不需要你寫的所有東西,但是我可以從中選擇我需要什麼來生成必要的記錄。謝謝! – fdmillion

+0

@fdmillion樂於助人! – SqlZim

0

主要的是你沒有提供任何樣本數據。 同樣在上面的腳本中,只有示例部分很重要。

我的意思是有必要了解子彈中的這些點。

如果我能理解正確,那麼它並不複雜。 其他告訴我什麼我不明白。

DECLARE @Shift table(empid int,starttime time(0),endtime time(0)) 
insert into @Shift VALUES(1,'9:00 AM','3 PM') 
--select * from @Shift 
declare @Appointment table(empid int,starttime time(0),endtime time(0)) 

insert into @Appointment VALUES(1,'10:00 AM','11:00 AM') 
,(1,'1:30 PM','2:00 PM') 
--select * from @Appointment 

declare @intervalInMin int=30 
;with GENCTE as 
(
select cast('12:00 AM' as time) as Caltime 
union ALL 
select dateadd(MINUTE,@intervalInMin,Caltime) 
from gencte 
where Caltime<=cast('23:00' as time) 
) 
select caltime AvailableBlockStartingTime 
from gencte g 
where exists(select empid from @Shift s 
where g.caltime between s.starttime and s.endtime 
and not EXISTS 
(
select empid from @Appointment a where s.empid=a.empid 
and g.caltime between a.starttime and a.endtime 
) 

)