背景
題目比較抽象,具體解釋一下。
有這麼一張表,裏面是多位客戶在不同時間的不同狀態。例如:
客戶 | 時間 | 狀態 |
---|---|---|
小老鼠 | 20200428 | 高興 |
小八戒 | 20200429 | 開心 |
小笨喵 | 20200501 | 悲傷 |
小老鼠 | 20200502 | 難受 |
小老鼠 | 20200503 | 相思 |
小八戒 | 20200504 | 懷舊 |
小笨喵 | 20200505 | 頭大 |
這裏多行數據比較混亂,想將多行數據按照標識分組,再改爲一行多列:
客戶 | 狀態 |
---|---|
小老鼠 | 20200428:高興 20200502 難受 20200503:相思 |
小八戒 | 20200429:開心 20200504:懷舊 |
小笨喵 | 20200501:悲傷 20200505:頭大 |
但是生產上往往不太需要這樣的彙總,更多時候是希望彙總最近一次狀態和上一次狀態,如圖:
客戶 | 最新狀態 | 上次狀態 |
---|---|---|
小老鼠 | 相思 | 難受 |
小八戒 | 懷舊 | 開心 |
小笨喵 | 頭大 | 悲傷 |
那麼具體要怎麼實現呢?
心路歷程
項目上線以後突然倍感壓力,這幾天休息的很差。然而項目上線並不算完,後續還需要對千萬數據提數取數。
由於當初設計的時候是面向頁面設計的,爲客戶crud方便考慮。但是後續領導告訴我還需要從數據庫中提數生成報表。這一下給我弄得手忙腳亂。
一個java開發工作,上需要改前端頁面,下需要搞數據提取,對於工作不足一年的我,真的有些難以接受。
抱怨許久,感慨萬分。
解決方案
本來以爲就是group_concat的事,沒想到,sybase竟然不支持。只得手寫存儲過程。
步驟
- 建表造數
首先爲了演示這個效果,我們根據以下語句建造數據來模擬這個過程。效果如圖所示:
代碼如下:
CREATE TABLE dbo.FRIEND_LOG
(
ID_ NUMERIC (19) NOT NULL,
FRIEND_NAME_ VARCHAR (32) NOT NULL,
DATE_ VARCHAR (8) NULL,
STATE_ VARCHAR (4) NULL,
CONSTRAINT PK_FRIEND_LOG PRIMARY KEY (ID_)
)
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (1, '小老鼠', '20200428', '高興')
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (2, '小八戒', '20200429', '開心')
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (3, '小笨喵', '20200501', '悲傷')
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (4, '小老鼠', '20200502', '難受')
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (5, '小老鼠', '20200503', '相思')
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (6, '小八戒', '20200504', '懷舊')
GO
INSERT INTO dbo.FRIEND_LOG (ID_, FRIEND_NAME_, DATE_, STATE_)
VALUES (7, '小笨喵', '20200505', '頭大')
GO
- 將數據傳到臨時表
爲了不破壞上表的結構以及數據的完整性,我們需要將FRIEND_LOG表中需要的內容傳至臨時表中。在創建臨時表前要先判斷是否已經存在相同名字的臨時表,若存在刪除即可。然後臨時表的結構分別爲角色名、狀態、所有狀態、狀態次數。
首先,角色名、狀態是FRIEND_LOG表中字段。
ALL_STATE_字段的存在是因爲我們需要把多行狀態遷移到一行中,故我們需要一個字段來存儲這些狀態。
而TIMES字段的設計目的則是爲了記錄各個角色的狀態數量,具體功能下面會詳解。
當執行完這些語句後,效果如圖:
代碼如下:
IF OBJECT_ID('#TEMP1') IS NOT NULL
drop table #TEMP1
GO
SELECT
FRIEND_NAME_,
STATE_,
space(40) AS ALL_STATE_,
0 as TIMES
INTO #TEMP1 FROM FRIEND_LOG ORDER BY FRIEND_NAME_ , DATE_ DESC
- 使用計數法插入
不要被標題嚇跑。所謂計數法,也不過是十以內數字加減法。
具體什麼原理?
其實很簡單,無非是一條一條遍歷數據,如果是第一次遇到這個角色,就直接將狀態寫入ALL_STATE_,TIMES記爲1,count也記爲1。如果是第n次,則一直將狀態追加至ALL_STATE_,count也依次累加,而TIMES則爲count+1。
我們用效果圖來解釋一下:
可以看到,第一次遍歷小八戒,ALL_STATE_寫入了“懷舊”狀態,而TIMES爲1,此時count也爲1。然後我們第二次記錄小八戒,此時狀態追加了“開心”,TIMES爲2,count也爲2。後面皆以此類推即可。
代碼如下:
declare @state varchar(400)
declare @id VARCHAR(32)
declare @count int
set @state=''
set @count=0
update #TEMP1
set ALL_STATE_=(case when @id =FRIEND_NAME_ then @state||STATE_ else STATE_ end)
,@state=(case when @id =FRIEND_NAME_ then @state||STATE_ else STATE_ end)
,TIMES=(case when @id =FRIEND_NAME_ then @count+1 else 1 end)
,@count=(case when @id =FRIEND_NAME_ then @count+1 else 1 end)
,@id =FRIEND_NAME_
- 分列查看
最後一步了,我們對臨時表的ALL_STATE_列進行分列查看皆可。通過substring函數,將其分到其他列。另外“(select FRIEND_NAME_, (case when max(TIMES) > 2 then 2 else max(TIMES) end)”語句是用取狀態次數爲2的,即最近兩次狀態,效果如圖:
此時代碼如下:
select t.FRIEND_NAME_,substring(t.ALL_STATE_,1,2) state1,substring(t.ALL_STATE_,3,2) state2,,TIMES
from #TEMP1 t inner join (select FRIEND_NAME_, (case when max(TIMES) > 2 then 2 else max(TIMES) end) as tl from #TEMP1 group by FRIEND_NAME_) c
on t.FRIEND_NAME_=c.FRIEND_NAME_ and t.TIMES=c.tl
另外,我們可以修改代碼來取消限制,但是一定要確保正確分列,效果如圖:
代碼如下:
select t.FRIEND_NAME_,substring(t.ALL_STATE_,1,2) state1,substring(t.ALL_STATE_,3,2) state2,substring(t.ALL_STATE_,5,2) state3,TIMES
from #TEMP1 t inner join (select FRIEND_NAME_, max(TIMES) as tl from #TEMP1 group by FRIEND_NAME_) c
on t.FRIEND_NAME_=c.FRIEND_NAME_ and t.TIMES=c.tl
附錄
將上述源碼粘貼至同一sql文件即可測試運行,另外,如果土豪,也可以直接下載附件~
sql文件