本次文章目的:
Mysql並沒有專門的中位數算法,而對於SQL不熟悉的人,書寫中位數,只能通過JAVA等語言實現。
並非推薦使用Mysql完成中位數計算,以下實現,僅為了通過算法解析的過程中,了解一些Mysql常用與不常用的功能、函數,並開拓思維。
當然,對於一些臨時性的要求,需要制作一些臨時性的算法測試、校驗、導出, 能使用Mysql完成這類算法,就凸顯出其效率。
說到中位數,我們就需要一批數據來進行測試和實現,創建如下表:
DROP TABLE IF EXISTS CaseRent; CREATE TABLE CaseRent( ID int(6) NOT NULL AUTO_INCREMENT, ResidentialAreaID int(6) DEFAULT NULL, CaseFrom varchar(30) DEFAULT NULL, Price int(6) DEFAULT NULL, PRIMARY KEY (ID) );
稱之為出租案例表,關鍵字段有:小區ID、案例來源及價格。
接下來通過隨機數來給出租案例表賦值:
INSERT INTO CaseRent (ResidentialAreaID, CaseFrom ,Price) SELECT ROUND((RAND()*100)+1),'鏈家在線',ROUND((RAND()*8000)+1000)
該語句包含知識點如下:
- 通過 INSERT INTO ... SELECT 進行賦值(用途廣泛,創建表亦可以使用)
- 運用Rand() 隨機數函數,ROUND() 四舍五入函數,完成小區ID從0~100 ,價格從1000~9000的隨機錄入。
一條數據當然不夠,我們可以使勁的多點幾下執行,使數據增加到近10條。這時候我們修改一下賦值語句
INSERT INTO CaseRent (ResidentialAreaID, CaseFrom ,Price) SELECT ROUND((RAND()*100)+1),'鏈家在線',ROUND((RAND()*8000)+1000) FROM CaseRent
繼續反復來N下,之后將來源“鏈家在線”修改為“房天下”,進行一次賦值。
INSERT INTO CaseRent (ResidentialAreaID, CaseFrom ,Price) SELECT ROUND((RAND()*100)+1),'房天下',ROUND((RAND()*8000)+1000) FROM CaseRent
模擬數據到此完成!示例如下:
實際上,網上的中位數花式百出,但無一不是:代碼篇幅長、需要自我關聯 或者 使用上臨時變量。
當然也有類似我們接下來要講的方式。無論哪種方式,都需要更多的了解和擴展自己所知。
接下來以剛才我們自定義的模擬數據為例子,安排第一個問題:
- 查找小區ID = 99 的價格中位數
這類的中位數,可以說是最簡單的,而且網上大部分中位數,均針對此類中位數(單條件),從上述網站就可以看到,其問題與我們的類似,但其代碼量可謂不少。
我們來分析問題:其獲取價格中位數,就必須使用ORDER BY 來實現排序,排序后,統計總條數,來獲取中間一條的價格作為結果(如果為偶數,可以取2條均值,亦可以取前一條 例如 6條數據,可以取第3、4 條進行均值計算,這里以取前一條為算法模擬)
那么第一步,無疑是要進行價格從小到大的排序:
SELECT * FROM CaseRent WHERE ResidentialAreaID = 99 ORDER BY Price
排序之后,ID顯的雜亂無章,關如此,我們人為的話,只能去手動數條數進行查找, 因此我們需要擁有一個新的自增ID,以此來更快的得知其對應的排名。
如何得到新的自增ID呢? 我們可以新建一張表, 通過INSERT INTO ...SELECT 來完成新數據的錄入,以此達到數據的ID自增:例如:
INSERT INTO NewCaseRent(ResidentialAreaID,CaseFrom,Price) SELECT ResidentialAreaID,CaseFrom,Price FROM CaseRent WHERE ResidentialAreaID = 99 ORDER BY Price
不過這樣我們就需要建表了,這就顯的很麻煩,因為一個自增,而新建一張表,入不敷出,
那么我們就需要一個變量,來實現自增功能。
同JAVA/Python等開發語言一樣,Mysql也有變量,通常以@開頭為用戶自定義變量,以@@開頭為系統變量。
那么我們怎么使用變量?很簡單,通過SET創建並賦值變量值, 再通過SELECT查詢結果,例如:
SET @ID = 0; SELECT @ID;
有了變量,我們可以將變量作為新的自增ID,來代替創建一張新表的操作了,
通過變量自加操作,完成新的自增ID功能:
SET @ID = 0; SELECT @ID:=@ID+1 AS ID,ResidentialAreaID,CaseFrom,Price FROM CaseRent WHERE ResidentialAreaID = 99 ORDER BY Price
注意幾點:
- 在SELECT中,給臨時變量賦值,使用 :=
- 每條語句,從底層講,都是循環查詢,因此在語句上直接自增,就可以實現逐條累加。
當然,上面的語句其實是2條語句,這樣放到JAVA或者其他語言中執行,可能不方便,因此也可以修改成如下語句:
SELECT @ID:=@ID+1 AS ID,ResidentialAreaID,CaseFrom,Price FROM CaseRent,(SELECT @ID:=0) b WHERE ResidentialAreaID = 99 ORDER BY Price
結果示例:
效果很好,接下來我們要做的,就是獲取ID=總條數/2 的那條數據了。
思考一下,如何才能簡單的得到結果?
SELECT * FROM ( SELECT @ID:=@ID+1 AS ID,ResidentialAreaID,CaseFrom,Price FROM CaseRent,(SELECT @ID:=0) b WHERE ResidentialAreaID = 99 ORDER BY Price ) a WHERE ID = @ID/2
通過簡單的中位數選取,深刻認知Mysql臨時變量的用法。
接下來引入加深層次的中位數:
- 根據案例來源,分別統計不同來源,小區ID=99的中位數。
分析問題:比第一步多了一個條件,其結果也多了一條數據。
那么該怎么做呢?
我們知道,排序的時候,需要按照 案例來源、價格 2個條件進行排序了,如果直接自增ID, 會是什么樣的呢?
SELECT @ID:=@ID+1 AS ID,ResidentialAreaID,CaseFrom,Price FROM CaseRent,(SELECT @ID:=0) b WHERE ResidentialAreaID = 99 ORDER BY CaseFrom,Price
很明顯,如果想要實現真確的自增ID, 到了鏈家在線這一步,ID需要重新從1開始計算。
那么難道我們分成2次統計? 如果案例來源有N個,這個方式明顯不行。
接下來引入Mysql函數 IF
IF ( 條件 , 真 , 假 )
為什么引入IF? 我們需要判斷排序后自增的時候,案例來源是否和上次的一樣,如果不一樣 說明切換到了新來源,這時候將@ID設置為從1開始,就可以實現2個來源不同的自增ID。
要判斷來源是否一樣,我們還得加個臨時變量 @CaseFrom
SET @ID:=0,@CaseFrom=''; SELECT IF(@CaseFrom!=CaseFrom,@ID:=1,@ID:=@ID+1) AS ID,ResidentialAreaID,CaseFrom,Price, @CaseFrom:=CaseFrom wy FROM CaseRent WHERE ResidentialAreaID = 99 ORDER BY CaseFrom,Price;
這里的wy字段,就純粹是為了賦值CaseFrom。對其他操作無用。
結果如下:
但是問題來了。 @ID已經不能直接用來 判斷Count(*)/2了 。 因為@ID 已經是鏈家在線的ID,而不是房天下的。
通過創建臨時表:臨時完美通俗的解決該問題:
臨時表Temporary只在當前會話使用,其余會話創建相同名稱臨時表,不互相沖突,不直接生成實體表。
但臨時表不能自我關聯。
SET @ID:=0,@CaseFrom=''; DROP TABLE IF EXISTS CS_1; CREATE TEMPORARY TABLE CS_1 SELECT IF(@CaseFrom!=CaseFrom,@ID:=1,@ID:=@ID+1) AS ID,ResidentialAreaID,CaseFrom,Price,@CaseFrom:=CaseFrom wy FROM CaseRent WHERE ResidentialAreaID = 99 ORDER BY CaseFrom,Price; DROP TABLE IF EXISTS CS_2; CREATE TEMPORARY TABLE CS_2 SELECT CaseFrom,FLOOR(Max(ID)/2) CenterID FROM CS_1 GROUP BY CaseFrom; SELECT * FROM CS_1 a INNER JOIN CS_2 b ON a.ID = b.CenterID AND a.CaseFrom=b.CaseFrom;
這就顯的拖沓了,寫了這么多代碼,創建了2張臨時表,關聯后獲取結果。 不過只是相對而言, 對於一些臨時性的操作,計算、導出的時候,就算是python編寫個腳本,其代碼量也遠遠大於這些。
上述方式,通過臨時表 + IF 的方式,實現了多層次的中位數獲取。但是我們知道,通過IF判斷,意味着我如果添加新的層次,例如:
- 獲取每一個小區、每一個來源的中位數。
這樣我們就得增加一個小區ID的臨時變量,不僅案例來源改變,需要重置ID為1, 小區ID改變時,也要重置為1, 這樣的代碼如下:
SET @ID:=0,@CaseFrom='',@ResidentialAreaID=0; DROP TABLE IF EXISTS CS_1; CREATE TEMPORARY TABLE CS_1 SELECT IF(@CaseFrom!=CaseFrom,@ID:=1,@ID:=@ID+1) AS ID, IF(@ResidentialAreaID!=ResidentialAreaID,@ID:=1,1) AS ID2, ResidentialAreaID,CaseFrom,Price,@CaseFrom:=CaseFrom wy,@ResidentialAreaID:=ResidentialAreaID wy2 FROM CaseRent ORDER BY ResidentialAreaID,CaseFrom,Price; DROP TABLE IF EXISTS CS_2; CREATE TEMPORARY TABLE CS_2 SELECT ResidentialAreaID,CaseFrom,FLOOR(Max(ID)/2) CenterID FROM CS_1 GROUP BY ResidentialAreaID,CaseFrom; SELECT * FROM CS_1 a INNER JOIN CS_2 b ON a.ID = b.CenterID AND a.CaseFrom=b.CaseFrom AND a.ResidentialAreaID=b.ResidentialAreaID;
多了一個IF判斷,多了一個臨時變量,多關聯了一個字段。
這對熟悉並了解該邏輯的人來說並沒有增加多少代碼量,但其多了一層邏輯,需要了解,這就可能照成混淆。
看上去很多,其實相較於其他方式,已經很精簡了,不過還沒完,我們還有很多方法可以嘗試!
例如編寫Mysql 自定義函數、存儲過程來實現,不過這就有點偏離了。
接下來換一種方式實現。
通過 GROUP_CONCAT 和 SUBSTRING_INDEX實現中位數算法
Group_concat 一般不會太陌生,一般伴隨着Group By 使用,當然也可以不實用Group by
通過Group_concat 可以將結果字段 默認通過 逗號 分割,組成一個新的字符串。
例如:
SELECT GROUP_CONCAT(Price) FROM CaseRent WHERE ResidentialAreaID = 99;
其結果如下:
而GROUP_CONCAT中,還可以寫一些SQL代碼。例如
GROUP_CONCAT( Price ORDER BY Price )
或者:
GROUP_CONCAT( DISTINCT Price )
是不是很方便,可以自行排序、剔除重復等操作,組成一個新的字符串。
再介紹另一個函數:SUBSTRING_INDEX
先看一下結果:
SELECT SUBSTRING_INDEX('一批,數,據',',',1)
= 一批
SELECT SUBSTRING_INDEX('一批,數,據',',',2)
= 一批,數
SELECT SUBSTRING_INDEX('一批,數,據',',',3)
= 一批,數,據
很明確了, 第一個參數放字符串,第二個為分割字符,第三個為取到第幾個字符。
那就再說一個 -1 , -1 很常見,Redis、python 中 分割、查找字符經常使用,意為反向取值, 例如:
SELECT SUBSTRING_INDEX('一批,數,據',',',-1)
= 據
結合這兩種函數的特性,就能完成中位數獲取了。
我們來看一下:
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(Price ORDER BY Price),',',Count(1)/2),',',-1) zws FROM CaseRent WHERE ResidentialAreaID = 99;
以上涉及了2個函數, SUBSTRING_INDEX 以及 GROUP_CONCAT,
通過GROUP_CONCAT將結果排序后組成逗號分割的新字符串, 並通過SUBSTRING_INDEX, 獲取到總量/2的結果,再通過SUBSTIRNG_INDEX -1的獲取倒數第一個值,即為中位數結果。
那么如果加上案例來源獲取中位數,這代碼會變成什么樣?
SELECT CaseFrom,SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(Price ORDER BY Price),',',Count(1)/2),',',-1) zws FROM CaseRent WHERE ResidentialAreaID = 99 Group By CaseFrom;
再加上區分小區呢?:
SELECT ResidentialAreaID,CaseFrom, SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(Price ORDER BY Price),',',Count(1)/2),',',-1) zws FROM CaseRent Group By ResidentialAreaID,CaseFrom;
似乎很簡單,但是GROUP_CONCAT有個默認承載長度 1024
如果不修改參數的情況下,做大量數據的中位數統計,會超出GROUP_CONCAT的承載長度,導致計算錯誤。
而一般情況下,我們無法修改服務器的Mysql配參,可以通過:
show variables like 'group_concat_max_len'
來參考當前參數。
以及:
-- 以當前會話,臨時修改GROUP_CONCAT支撐長度。
SET @@GROUP_CONCAT_MAX_LEN = 1024000;
當然,如果有必要,可以直接通知運維修改一下參數長度,如果不常用,可以自行使用這種方式修改后臨時使用;因此數據量大的情況下,正確的寫法如下:
SET @@GROUP_CONCAT_MAX_LEN = 1024000; SELECT ResidentialAreaID,CaseFrom, SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(Price ORDER BY Price),',',Count(1)/2),',',-1) zws FROM CaseRent Group By ResidentialAreaID,CaseFrom;
到此,中位數算法結束。
主要知識點:
臨時變量
臨時表
系統變量
IF
GROUP_CONCAT
SUBSTRING_INDEX