SQL疑難問題


      最近,遇到並解決一個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

  

      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM