Mysql:實現中位數算法


本次文章目的:

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)

 

該語句包含知識點如下:

  1. 通過 INSERT INTO ... SELECT 進行賦值(用途廣泛,創建表亦可以使用)
  2. 運用Rand() 隨機數函數,ROUND() 四舍五入函數,完成小區ID0~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

 

模擬數據到此完成!示例如下:

 

 

實際上,網上的中位數花式百出,但無一不是:代碼篇幅長、需要自我關聯 或者 使用上臨時變量。

當然也有類似我們接下來要講的方式。無論哪種方式,都需要更多的了解和擴展自己所知。

 

接下來以剛才我們自定義的模擬數據為例子,安排第一個問題:

  1. 查找小區ID = 99 的價格中位數

這類的中位數,可以說是最簡單的,而且網上大部分中位數,均針對此類中位數(單條件),從上述網站就可以看到,其問題與我們的類似,但其代碼量可謂不少。

 

我們來分析問題:其獲取價格中位數,就必須使用ORDER BY 來實現排序,排序后,統計總條數,來獲取中間一條的價格作為結果(如果為偶數,可以取2條均值,亦可以取前一條 例如 6條數據,可以取第34 條進行均值計算,這里以取前一條為算法模擬)

 

那么第一步,無疑是要進行價格從小到大的排序:

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

注意幾點:

  1. SELECT中,給臨時變量賦值,使用 :=
  2. 每條語句,從底層講,都是循環查詢,因此在語句上直接自增,就可以實現逐條累加。

 

當然,上面的語句其實是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臨時變量的用法。

 

 

接下來引入加深層次的中位數:

  1. 根據案例來源,分別統計不同來源,小區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判斷,意味着我如果添加新的層次,例如:

  1. 獲取每一個小區、每一個來源的中位數。

 

這樣我們就得增加一個小區ID的臨時變量,不僅案例來源改變,需要重置ID1, 小區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 很常見,Redispython 中  分割、查找字符經常使用,意為反向取值, 例如:

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


免責聲明!

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



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