iT邦幫忙

0

玩玩SQL~查詢當月排班各區間的班別~求SQL改善

  • 分享至 

  • xImage

恩~遇到一個不知道如何用純SQL寫法的當月排班班別區間/images/emoticon/emoticon38.gif

只能想到用T-SQL來達成寫法...

先給前置資料的SQL

declare @Shift table(
	Name nvarchar(20)
	,SetDate date
	,Code nvarchar(5)
)
insert into @Shift
values('小明','2024/10/1','A'),('小明','2024/10/2','A'),('小明','2024/10/3','A'),('小明','2024/10/4','A')
,('小明','2024/10/5','休'),('小明','2024/10/6','休'),('小明','2024/10/7','A'),('小明','2024/10/8','A')
,('小明','2024/10/9','A'),('小明','2024/10/10','休'),('小明','2024/10/11','A'),('小明','2024/10/12','休')
,('小明','2024/10/13','休'),('小明','2024/10/14','B'),('小明','2024/10/15','B'),('小明','2024/10/16','B')
,('小明','2024/10/17','B'),('小明','2024/10/18','B'),('小明','2024/10/19','休'),('小明','2024/10/20','休')
,('小明','2024/10/21','A'),('小明','2024/10/22','A'),('小明','2024/10/23','A'),('小明','2024/10/24','A')
,('小明','2024/10/25','A'),('小明','2024/10/26','休'),('小明','2024/10/27','休'),('小明','2024/10/28','A')
,('小明','2024/10/29','A'),('小明','2024/10/30','A'),('小明','2024/10/31','A')
,('大白','2024/10/1','特'),('大白','2024/10/2','特'),('大白','2024/10/3','B'),('大白','2024/10/4','B')
,('大白','2024/10/5','休'),('大白','2024/10/6','休'),('大白','2024/10/7','B'),('大白','2024/10/8','B')
,('大白','2024/10/9','B'),('大白','2024/10/10','休'),('大白','2024/10/11','B'),('大白','2024/10/12','休')
,('大白','2024/10/13','休'),('大白','2024/10/14','A'),('大白','2024/10/15','A'),('大白','2024/10/16','A')
,('大白','2024/10/17','A'),('大白','2024/10/18','A'),('大白','2024/10/19','休'),('大白','2024/10/20','休')
,('大白','2024/10/21','B'),('大白','2024/10/22','B'),('大白','2024/10/23','B'),('大白','2024/10/24','B')
,('大白','2024/10/25','B'),('大白','2024/10/26','休'),('大白','2024/10/27','休'),('大白','2024/10/28','B')
,('大白','2024/10/29','B'),('大白','2024/10/30','B'),('大白','2024/10/31','B')

--A代表 08-16 早班時段
--B代表 13-21 午班時段

--想要的結果: 
--大白 , B:1~13、A:14~20、B:21~31
--小明 , A:1~13、B:14~20、A:21~31

我用T-SQL後,得到此結果~
https://ithelp.ithome.com.tw/upload/images/20241013/200613697z5hUIEFjo.png

看有哪位高手比較厲害的SQL寫法...^^

我再分享我寫的T-SQL/images/emoticon/emoticon06.gif

好像我表達的不好~我貼出T-SQL好了~看如何改善成為純SQL

我也把直轉橫列出~比較看得懂..

select *
from (
	select Name
	,day(SetDate) DayNum
	,Code
	from @Shift
) k
pivot (
	max(Code)
	for DayNum in(
		[1],[2],[3],[4],[5],[6],[7],[8],[9],[10]
		,[11],[12],[13],[14],[15],[16],[17],[18]
		,[19],[20],[21],[22],[23],[24],[25],[26]
		,[27],[28],[29],[30],[31]
	)
) p

https://ithelp.ithome.com.tw/upload/images/20241013/200613698aziKAbJi7.png

T-SQL語法的寫法....0.0

declare @ShiftMonth table(
	Name nvarchar(20)
	,MonthCode nvarchar(max)
)

declare @ShiftName table(
	Sort int
	,Name nvarchar(20)
)

declare @ShiftSet table(
	Name nvarchar(20)
	,SetDate date
	,Code nvarchar(5)
	,LastDate date
	,LastCode nvarchar(5)
)

declare @Count int = 0
declare @Code nvarchar(5) = ''
declare @LastCode nvarchar(5) = ''
declare @StartDate date,@EndDate date
declare @UpdStartDate date,@UpdEndDate date
declare @i int = 0,@n int=0
declare @MonthCode nvarchar(max) = ''
declare @MonthStart date = '2024/10/1'
declare @MonthEnd date = dateadd(day,-1,dateadd(month,1,@MonthStart))

insert into @ShiftName
select Row_Number()Over(order by Name) Sort
,Name
from @Shift
where SetDate between @MonthStart and @MonthEnd
group by Name

declare @NameCount int = isNull((select count(0) from @ShiftName),0)
declare @Name nvarchar(20)
declare @k int = 0

while(@k<@NameCount)
begin
	set @k = @k + 1
	select @Name=Name
	from @ShiftName
	where Sort = @k

	insert into @ShiftSet
	select *
	,isNull((
		select top 1 b.SetDate
		from @Shift b
		where a.Name = b.Name
		and Code not in('休','特')
		and a.SetDate < b.SetDate
	),a.SetDate) LastDay
	,isNull((
		select top 1 b.Code
		from @Shift b
		where a.Name = b.Name
		and Code not in('休','特')
		and a.SetDate < b.SetDate
	),a.Code) LastCode
	from @Shift a
	where Code not in('休','特')
	and Name = @Name

	set @i = 0
	set @n = 0
	set @Count = isNull((select count(0) from @ShiftSet),0)
	set @MonthCode = ''

	set @StartDate = @MonthStart

	while(@i<@Count)
	begin
		set @i = @i + 1

		if(@i=1)
		begin
			select top 1 @EndDate=LastDate
			,@Code=Code
			,@LastCode=LastCode
			from @ShiftSet
			order by SetDate
		end

		select @UpdStartDate=SetDate
		,@UpdEndDate=dateadd(day,-1,LastDate)
		,@Code=Code
		,@LastCode=LastCode
		from (
			select Row_Number()Over(order by SetDate) Sort
			,*
			from @ShiftSet
		) k
		where Sort = @i
		and Code<>LastCode

		if(@Code<>@LastCode)
		begin
			set @n = @n + 1
			set @EndDate = @UpdEndDate
			if(@MonthCode='')
			begin
				set @MonthCode = Convert(varchar,@Code) + ':' + Convert(varchar,Day(@StartDate)) + '~' + Convert(varchar,Day(@EndDate))
			end
			else
			begin
				set @MonthCode = @MonthCode + ',' + Convert(varchar,@Code) + ':' + Convert(varchar,Day(@StartDate)) + '~' + Convert(varchar,Day(@EndDate))
			end
			set @StartDate = dateadd(day,1,@EndDate)
			set @Code = @LastCode
		end
	end

	if(@Count>0)
	begin
		select @EndDate=max(SetDate)
		from @Shift
		where Name = @Name

		if(@MonthCode='')
		begin
			set @MonthCode = Convert(varchar,@Code) + ':' + Convert(varchar,Day(@StartDate)) + '~' + Convert(varchar,Day(@EndDate))
		end
		else
		begin
			set @MonthCode = @MonthCode + ',' + Convert(varchar,@Code) + ':' + Convert(varchar,Day(@StartDate)) + '~' + Convert(varchar,Day(@EndDate))
		end

		insert into @ShiftMonth 
		select @Name
		,@MonthCode

		delete from @ShiftSet
	end


end

select *
from @ShiftMonth

看更多先前的討論...收起先前的討論...
不知道你是要怎樣處理,不知道邏輯,怎麼寫.
邏輯:假如10月排班
小明 ,
排A班:1號到13號(含休假)、
排B班:14號~20號(含休假)、
排A班:21號~31號(含休假)

排班順序是按照日期大小排序的

其實就是把所有人當月排班狀態列出來@@...
rogeryao iT邦超人 7 級 ‧ 2024-10-13 16:12:34 檢舉
我還以為只有我看不懂
哈~畢竟是實例~拿出來玩看看~
看有無高手解法有什麼厲害的寫法0.0a
可能我敘述題目方式~
邏輯有點不通吧@@...
但實際上~主管想知道
小明幾號到幾號 上A班時段,還是B班時段...
雖然從排班表看的很清楚~
但是在列出出勤報表相關紀錄的時候~
會濃縮在一個欄位紀錄出來@@"
rogeryao iT邦超人 7 級 ‧ 2024-10-13 19:40:48 檢舉
若以第一張貼圖為結果的話
我的資料結構會跟你不一樣
Code : 只會填入 A ,B ,C, D, E
意思是該員工本來排定的班別

另外新增一個欄位
Holiday :休 or 特
第二張圖只是當 Holiday 有值時將 Code 顯示成 Holiday
這樣問題就變得比較單純

不然你的第一張圖就應該顯示 休 or 特是哪幾天
否則主管會以為該員工當天有上班

或許有一天你會碰到有兩條生產線
一條用 A , B
一條用 C , D , E
那麼你的 T-SQL 或許會更複雜
恩~畢竟那個資料結構~是前人留下的遺產
他們也是用很久了~我也只能照現有結構去變化^^a
【休 or 特】有另外的報表欄位顯示,會顯示請假有哪些項目
特休 2d0h、事假 1d0h、病假 0d4h、補休 0d4h、生理假 0d4h...幾天等等
我那個報表欄位單純只看班別資料的~
所以這個看起來無法純SQL處理@@..
只能用T-SQL來硬幹了^.^a

出勤報表欄位會分開一些項目~
不會把當月班別、休假之類都擠在一起~
【當月班別】【當月請假】【之類等等...】

>>或許有一天你會碰到有兩條生產線
早就更複雜了~
他們A1 A2 A3 上班時間都會早1小時或晚1小時~
或者廚師上班 A4 時段是 08-13 很短的時長呢
只是都會通稱為A班時段~
所以像這種要求同一個名稱叫A班~
但因配合同事時間會有點差異~
畢竟系統還要抓他們上下班有無遲到會早退的計算呢~
rogeryao iT邦超人 7 級 ‧ 2024-10-13 21:31:33 檢舉
如果
SetDate date ,
Code nvarchar(5)

這兩個欄位分屬不同的 Table , 那也會簡單一些
可以~你可以試試你的方式~寫寫看@@
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
一級屠豬士
iT邦大師 1 級 ‧ 2024-10-13 20:59:25
最佳解答

按我前往

with t1 as (
select boy, sdate
     , case when code = '休' or  code = '特' then null
       else code
       end
  from it1013
), t2 as (
select *
     , sum(case when code is null then 0 else 1 end) over(order by boy, sdate) as code_grp
  from t1
), t3 as (
select boy, sdate
     , first_value(code) over(partition by code_grp order by sdate) as new_code
  from t2
), t4 as (
select boy, sdate, new_code
     , sum(case when new_code is null then 0 else 1 end) over(order by boy, sdate desc) as new_code_grp
  from t3
), t5 as (
select boy, sdate
     , first_value(new_code) over(partition by new_code_grp order by sdate desc) as new_code2
  from t4
), t6 as (
select boy, sdate
     , new_code2
     , lag(new_code2) over(partition by boy order by sdate) lagcode
  from t5
), t7 as (
select boy, sdate, new_code2 as code
     , case when new_code2 = 'A' and lagcode = 'B' then true
            when new_code2 = 'B' and lagcode = 'A' then true
            else false
       end as cut
  from t6
), t8 as (
select boy, sdate, code, date_part('day', sdate)::int as cutdate
  from t7
 where cut
    or date_part('day', sdate)::int = 1
), t9 as (
select boy, code, cutdate, coalesce(lead(cutdate) over (partition by boy order by sdate) - 1, 31) as enddate
  from t8
) , t10 as (
select boy
     , cutdate
     , code || ':' || cutdate::text || '~' || enddate::text as datestr
  from t9
)
select boy
     , string_agg(datestr, ',' order by cutdate)
  from t10
 group by boy;

result:

 boy  |       string_agg
------+------------------------
 大白 | B:1~13,A:14~20,B:21~31
 小明 | A:1~13,B:14~20,A:21~31
(2 rows)

看更多先前的回應...收起先前的回應...

完整的應該是在 coalesce(lead(cutdate) over (partition by boy order by sdate) - 1, 31)
這裡的31 要用日期函數計算出當月的最後一天.
但是我們使用的DB 不同, 這對純真的人大大來說,小菜一碟,所以我就偷懶直接使用31了.

挖~~SQL化厲害~

用 with 這個的確應用~我還沒有熟練XD..

用 window function, sum(), first_value(), lag(), lead(), 組合拳.string_agg() 聚合.
先把 休假 變成 null
sum(), first_value() 來 填充 null, 正向一次,反向一次(大白 月初兩天), 然後lag(), 然後判斷差異,算出分切點日期,
然後再將 分切點日期, lead 上去 減一天 得到 每組的區間,
然後將區間變成 sdate~edate ,然後 string_agg 聚合起來.

好的~我想想with如何應用~謝謝

0
pilipala
iT邦研究生 5 級 ‧ 2024-10-13 21:05:47

正常流程應該是,理論班表 => 實際出勤班表 => 幫 HR 勾稽兩者異常之處,讓 HR 去釐清判斷是否合理

休假日前後是 A、B 班別時,有邏輯可以判斷是歸屬哪個班別嗎?

https://ithelp.ithome.com.tw/upload/images/20241013/20122986Tzp8D7vjzx.jpg

班別是若遇當月休假換班別~就歸屬前一個班別~
例如 10/13 休 14/14 換成A班
那麼 10/13 會往前 抓到 最後一天的班別 若是B班~
那麼10/13就算B班薪資

但是像月初就特休就會往後抓班別來判斷

至於HR勾稽~他們輪班比較沒辦做吧~
因為他們要排下個月班時~
會一個組討論~哪天誰可以上班~誰有事休假~
過年是沒有休假的~用輪班處理(會給特別加班費)

我這個題目範例是用固定排班來敘述的~
不是用輪班比較麻煩輸入@@...

1
rogeryao
iT邦超人 7 級 ‧ 2024-10-13 23:24:33

Code : 只有班別代號,不含休假或特休

CREATE TABLE da (
Name nvarchar(20),
SetDate date,
Code nvarchar(5));

INSERT INTO da (Name, SetDate,Code) VALUES
(N'小明','2024/10/1','A'),(N'小明','2024/10/2','A'),(N'小明','2024/10/3','A'),(N'小明','2024/10/4','A')
,(N'小明','2024/10/5',N'A'),(N'小明','2024/10/6',N'A'),(N'小明','2024/10/7','A'),(N'小明','2024/10/8','A')
,(N'小明','2024/10/9','A'),(N'小明','2024/10/10',N'A'),(N'小明','2024/10/11','A'),(N'小明','2024/10/12',N'A')
,(N'小明','2024/10/13',N'A'),(N'小明','2024/10/14','B'),(N'小明','2024/10/15','B'),(N'小明','2024/10/16','B')
,(N'小明','2024/10/17','B'),(N'小明','2024/10/18','B'),(N'小明','2024/10/19',N'B'),(N'小明','2024/10/20',N'B')
,(N'小明','2024/10/21','A'),(N'小明','2024/10/22','A'),(N'小明','2024/10/23','A'),(N'小明','2024/10/24','A')
,(N'小明','2024/10/25','A'),(N'小明','2024/10/26',N'A'),(N'小明','2024/10/27',N'A'),(N'小明','2024/10/28','A')
,(N'小明','2024/10/29','A'),(N'小明','2024/10/30','A'),(N'小明','2024/10/31','A')
,(N'大白','2024/10/1',N'B'),(N'大白','2024/10/2',N'B'),(N'大白','2024/10/3','B'),(N'大白','2024/10/4','B')
,(N'大白','2024/10/5',N'B'),(N'大白','2024/10/6',N'B'),(N'大白','2024/10/7','B'),(N'大白','2024/10/8','B')
,(N'大白','2024/10/9','B'),(N'大白','2024/10/10',N'B'),(N'大白','2024/10/11','B'),(N'大白','2024/10/12',N'B')
,(N'大白','2024/10/13',N'B'),(N'大白','2024/10/14','A'),(N'大白','2024/10/15','A'),(N'大白','2024/10/16','A')
,(N'大白','2024/10/17','A'),(N'大白','2024/10/18','A'),(N'大白','2024/10/19',N'A'),(N'大白','2024/10/20',N'A')
,(N'大白','2024/10/21','B'),(N'大白','2024/10/22','B'),(N'大白','2024/10/23','B'),(N'大白','2024/10/24','B')
,(N'大白','2024/10/25','B'),(N'大白','2024/10/26',N'B'),(N'大白','2024/10/27',N'B'),(N'大白','2024/10/28','B')
,(N'大白','2024/10/29','B'),(N'大白','2024/10/30','B'),(N'大白','2024/10/31','B');
WITH CTE01 AS (SELECT M.Name,M.SetDate,M.Code,1 + SUM(M.Num) OVER (
PARTITION BY M.Name ORDER BY M.SetDate) AS No
FROM (
SELECT da.*,
CASE WHEN Code = LAG(Code,1,Code) OVER (
PARTITION BY Name ORDER BY SetDate) THEN 0 ELSE 1 END AS Num
FROM da) M),  
CTE02 AS (
SELECT Name,Code,No,Min(SetDate) AS MinDate,Max(SetDate) AS MaxDate
FROM CTE01
GROUP BY Name,Code,No),
CTE03 AS (
SELECT Name,No,Code,Code+':'+Right(MinDate,2)+'~'+Right(MaxDate,2) AS PartStr
FROM CTE02)

SELECT Name,STRING_AGG(PartStr,',') WITHIN GROUP (ORDER BY No) AS AllStr
FROM CTE03
GROUP BY Name

Demo

休假或特休

CREATE TABLE dc (
Name nvarchar(20),
SetDate date,
Holiday nvarchar(5));

INSERT INTO dc (Name, SetDate,Holiday) VALUES
(N'小明','2024/10/5',N'休'),
(N'小明','2024/10/6',N'休'),
(N'小明','2024/10/10',N'休'),
(N'小明','2024/10/12',N'休'),
(N'小明','2024/10/13',N'休'),
(N'小明','2024/10/19',N'休'),
(N'小明','2024/10/20',N'休'),
(N'小明','2024/10/26',N'休'),
(N'小明','2024/10/27',N'休'),
(N'大白','2024/10/1',N'特'),
(N'大白','2024/10/2',N'特'),
(N'大白','2024/10/5',N'休'),
(N'大白','2024/10/6',N'休'),
(N'大白','2024/10/10',N'休'),
(N'大白','2024/10/12',N'休'),
(N'大白','2024/10/13',N'休'),
(N'大白','2024/10/19',N'休'),
(N'大白','2024/10/20',N'休'),
(N'大白','2024/10/26',N'休'),
(N'大白','2024/10/27',N'休');
SELECT F.Name,F.SetDate,F.Holiday,1 + SUM(F.HNum) OVER (
PARTITION BY F.Name ORDER BY F.SetDate) AS HNo
FROM (
SELECT dc.*,
CASE WHEN Holiday = LAG(Holiday,1,Holiday) OVER (
PARTITION BY Name ORDER BY SetDate) THEN 0 ELSE 1 END AS HNum
FROM dc) F

Demo
休假或特休 dc 與 da left join 應該可以產生出圖二,
此處 SQL 只便於找出休假或特休群組,不在贅述。

參考資料 : SQL 依時間排序 連續資料的分組

喔~
你採用沒有混合休假的排班~
純粹班別哪到哪~的確是很好查詢
休假有另外的資料表紀錄在混合進來~

可惜他們班表功能是這樣混合的

https://ithelp.ithome.com.tw/upload/images/20241014/20061369il92hfNuOX.png

rogeryao iT邦超人 7 級 ‧ 2024-10-14 00:42:12 檢舉

班別是若遇當月休假換班別~就歸屬前一個班別~
例如 10/13 休 14/14 換成A班
那麼 10/13 會往前 抓到 最後一天的班別 若是B班~
那麼10/13就算B班薪資

『那麼 10/13 會往前 抓到 最後一天的班別 若是B班~』
其實,這樣的論述可能是有問題的
一般來說應該是先有排班表,不同的班別薪資也會不一樣
10/13 請假時排到什麼班表,就扣除那個班表的錢
而不是往前推最後一次有上班的班表
仔細算薪資會有差別

或許有一天會發生
10/13 往前推是 B 班
但是人事部門算薪資時是用 A 班計算

這個就看人資訂下規則為準囉(通常會發公告,或者寫在員工手冊裡面)~
我們看他怎麼解釋~就怎樣設定了~

我要發表回答

立即登入回答