MySQL實現中位元演算法

來源:互聯網
上載者:User

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() 四捨五入函數,完成小區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

類比資料到此完成!樣本如下:

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

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

接下來以剛才我們自訂的類比資料為例子,安排第一個問題:
1.尋找小區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

注意幾點:
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的臨時變數,不僅案例來源改變,需要重設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

本文永久更新連結地址:https://www.bkjia.com/Linux/2018-03/151272.htm

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.