最近,遇到並解決一個SQL上的疑難問題。考勤系統,記錄着員工進出公司的刷卡記錄。而員工刷卡並不規范,存在刷多次的情況。例如:出去時連續刷多次,進來時也連續刷多次。篩選有效刷卡記錄數據的規則:對於出去時連續刷多次(包含一次)的情況,取第一次刷卡記錄;對於進來時連續刷多次(包含一次)的情況,取最后一次的刷卡記錄。考勤系統的數據量很大,假設公司有2萬名員工,一員工一天100條刷卡記錄。
用什么方法可以高效地查出某一時間范圍內員工的有效刷卡記錄?
測試表及測試數據如下:
create table Attendance ( UserId int, --員工ID ClockInTime datetime, --員工刷卡時間 Flag char(1) --進出標志 '1'代表出,'0'代表進 ) insert Attendance values(100001,'2015-06-01 08:03:00','1'), (100001,'2015-06-01 08:03:10','1'), (100001,'2015-06-01 08:03:50','1'), (100001,'2015-06-01 08:04:00','1'), (100001,'2015-06-01 08:10:00','0'), (100001,'2015-06-01 08:10:10','0'), (100001,'2015-06-01 08:15:00','1'), (100001,'2015-06-01 08:30:00','1'), (100001,'2015-06-01 08:40:10','0'), (100001,'2015-06-01 09:00:00','1'), (100001,'2015-06-01 09:15:10','0'), (100001,'2015-06-01 09:30:00','1'), (100002,'2015-06-01 08:03:00','0'), (100002,'2015-06-01 08:03:10','0'), (100002,'2015-06-01 08:03:50','1'), (100002,'2015-06-01 08:04:00','1'), (100002,'2015-06-01 08:10:00','1'), (100002,'2015-06-01 08:10:10','0'), (100002,'2015-06-01 08:15:00','1'), (100002,'2015-06-01 08:30:00','1'), (100002,'2015-06-01 08:40:10','1'), (100002,'2015-06-01 09:00:00','0'), (100002,'2015-06-01 09:15:10','0'), (100002,'2015-06-01 09:30:00','1')
而需篩選的有效數據為紅色標記部分:
而作為測試數據,也就只提供兩名員工,每人一天12條的刷卡記錄,需要完成的是將紅色標記的數據篩選出來。
不難看出問題的難點在於判斷哪些數據是連續(進或出)的,無論出去還是進來。把這一點解決了,所有的問題也就迎刃而解。
1)首先,想到了遞歸查詢。但是很快否定了想法,這個方法判斷不出來數據是否連續。
2)其次,又考慮游標。或許游標能判斷是否連續的問題,但是處理大數據量時,性能肯定極其低。
最后,閃現一個思路,沒想到順着這個思路把問題解決了。
1,先按UserID、日期分組,組內按ClockInTime升序排列。
select *, ROW_NUMBER() over(partition by UserId,convert(varchar(10),ClockInTime,23) order by ClockInTime) as RN into #tmp from Attendance
select * from #tmp order by UserId,ClockInTime
結果如圖:
2,再按UserID、日期、Flag分組,組內按ClockInTime升序排列。
select *, ROW_NUMBER() over(partition by UserId,convert(varchar(10),ClockInTime),Flag order by ClockInTime) as RN1 into #tmp1 from Attendance
select * from #tmp1 order by UserId,ClockInTime
結果如圖:
3,用#tmp1中的RN1與#tmp中的RN做差。
select a.*,b.RN1,b.RN1-a.RN as RN2 into #tmp2 from #tmp as a,#tmp1 as b where a.UserId=b.UserId and a.ClockInTime=b.ClockInTime and a.Flag=b.Flag
select * from #tmp2 order by UserId,ClockInTime
結果如圖:
3,根據UserID、日期、Flag、RN2可以判斷出哪些數據是連續的,然后,用Row_Number()排序一下,就可以篩選所需要的數據。
select *, case when Flag='1' then ROW_NUMBER() over(Partition by UserID,convert(varchar(10),ClockInTime,23),Flag,RN2 order by ClockInTime) else ROW_NUMBER() over(Partition by UserID,convert(varchar(10),ClockInTime,23),Flag,RN2 order by ClockInTime desc) end as RId into #tmp3 from #tmp2
select * from #tmp3 order by UserId,ClockInTime
結果如圖:
4,RID=‘1’的數據是正確的結果,即有效的刷卡記錄數據。
select UserId,ClockInTime,Flag from #tmp3 where Rid='1' order by UserId,ClockInTime
結果如圖:
這樣問題就解決了。進一步優化sql,其實1,2,3等3個步驟只要一步就解決了
select *, ROW_NUMBER() over(partition by UserId,convert(varchar(10),ClockInTime,23) order by ClockInTime)-ROW_NUMBER() over(partition by UserId,convert(varchar(10),ClockInTime),Flag order by ClockInTime) as RN from Attendance order by UserId,ClockInTime
有了上面查詢的結果,后面的查詢也簡單多了。如果用一句SQL來解決的話,那就是:
select UserId,ClockInTime,Flag from ( select *, case when Flag='1' then ROW_NUMBER() over(Partition by UserID,convert(varchar(10),ClockInTime,23),Flag,RN order by ClockInTime) else ROW_NUMBER() over(Partition by UserID,convert(varchar(10),ClockInTime,23),Flag,RN order by ClockInTime desc) end as RId from ( select *, ROW_NUMBER() over(partition by UserId,convert(varchar(10),ClockInTime,23) order by ClockInTime)-ROW_NUMBER() over(partition by UserId,convert(varchar(10),ClockInTime),Flag order by ClockInTime) as RN from Attendance ) as a ) as b where RId='1' order by UserId,ClockInTime