動態mysql行列轉換
阿新 • • 發佈:2019-01-24
一,列變成行例子演示
1,準備測試資料
USE csdn; DROP TABLE IF EXISTS flow_table; CREATE TABLE `flow_table` ( `ID` INT(11) NOT NULL AUTO_INCREMENT, `Number` BIGINT(11) NOT NULL, `City` VARCHAR(10) NOT NULL, `wm201403` DECIMAL(7,2) DEFAULT NULL, `wm201404` DECIMAL(7,2) DEFAULT NULL, `wm201405` DECIMAL(7,2) DEFAULT NULL, `wm201406` DECIMAL(7,2) DEFAULT NULL, `wm201407` DECIMAL(7,2) DEFAULT NULL, `wm201408` DECIMAL(7,2) DEFAULT NULL, PRIMARY KEY (`ID`,`Number`) ) ENGINE=INNODB DEFAULT CHARSET=utf8; INSERT INTO flow_table(Number,City,wm201403,wm201404,wm201405,wm201406,wm201407,wm201408)SELECT 1,'shanghai',100.2,180.4,141,164,124,127; INSERT INTO flow_table(Number,City,wm201403,wm201404,wm201405,wm201406,wm201407,wm201408)SELECT 2,'shanghai',110.23,180.34,141.23,104.78,124.67,127.45; INSERT INTO flow_table(Number,City,wm201403,wm201404,wm201405,wm201406,wm201407,wm201408)SELECT 3,'beijing',123.23,110.34,131.33,154.58,154.67,167.45; INSERT INTO flow_table(Number,City,wm201403,wm201404,wm201405,wm201406,wm201407,wm201408)SELECT 4,'hangzhou',0,110.34,131.33,154.58,154.67,0; INSERT INTO flow_table(Number,City,wm201405,wm201406,wm201407,wm201408)SELECT 5,'hangzhou',131.33,154.58,154.67,0;
2,儲存過程遍歷:
這個儲存過程建立了2張臨時表,查詢測試表資料形成遊標,遍歷遊標根據主鍵Number來呼叫pro_flow_modify儲存過程進行行列變化。程式碼如下:
DROP PROCEDURE IF EXISTS test.`proc_all_changes`; CREATE PROCEDURE test.proc_all_changes() BEGIN DECLARE v_number BIGINT; DECLARE v_city VARCHAR(10); DECLARE _done INT DEFAULT 0; /*定義遊標*/ DECLARE cur_all CURSOR FOR SELECT Number,City FROM test.`flow_table`; /**這裡如果需要定義下當NOT FOUND的時候,EXIT退出遊標遍歷,不然如果設定成CONTINUE會一直執行下去。*/ DECLARE EXIT HANDLER FOR NOT FOUND BEGIN SET _done=1;END; /*建立臨時表,存放所有欄位的臨時表*/ DROP TABLE IF EXISTS flow_n_columns; CREATE TABLE `flow_n_columns` ( `column_name` VARCHAR(10) NOT NULL ) ENGINE=INNODB DEFAULT CHARSET=utf8; /*存放最終變成行的資料表*/ DROP TABLE IF EXISTS flow_tmp; CREATE TABLE `flow_tmp` ( `Number` INT(11) DEFAULT NULL, `City` VARCHAR(10) DEFAULT NULL, `wm_str` VARCHAR(10) DEFAULT NULL, `Wm` DECIMAL(7,2) DEFAULT NULL ) ENGINE=INNODB DEFAULT CHARSET=utf8; OPEN cur_all; REPEAT FETCH cur_all INTO v_number, v_city; IF NOT _done THEN CALL test.pro_flow_modify(v_number,v_city); END IF; UNTIL _done=1 END REPEAT; CLOSE cur_all; /*展示下所有的行轉列的資料**/ SELECT * FROM test.flow_tmp; END
3,行裡變化儲存過程
通過查詢系統表information_schema.`COLUMNS`來獲取測試表flow_table的所有列,然後寫動態SQL,來把列的值錄入到臨時表flow_tmp中。
DROP PROCEDURE IF EXISTS test.`pro_flow_modify`; CREATE PROCEDURE test.`pro_flow_modify`(p_Number INT,p_city VARCHAR(10)) BEGIN DECLARE v_column_name VARCHAR(10) DEFAULT ''; DECLARE v_exe_sql VARCHAR(1000) DEFAULT ''; DECLARE v_start_wm VARCHAR(10) DEFAULT ''; DECLARE v_end_wm VARCHAR(10) DEFAULT ''; DECLARE v_num DECIMAL(10,2) DEFAULT 0; DECLARE i INT DEFAULT 1; DECLARE v_Number INT DEFAULT 0; SET v_Number=p_Number; DELETE FROM test.flow_n_columns; DELETE FROM test.flow_tmp WHERE Number=v_Number; /*把測試表flow_table的所有欄位都錄入欄位臨時表中,這樣就達到了從列變成行的目的*/ INSERT INTO flow_n_columns SELECT t.`COLUMN_NAME` FROM information_schema.`COLUMNS` t WHERE t.`TABLE_NAME`='flow_table' AND t.`TABLE_SCHEMA`='test' AND t.`COLUMN_NAME` NOT IN('ID','Number','City'); SELECT column_name INTO v_column_name FROM test.flow_n_columns LIMIT 1; /*開始迴圈遍歷欄位臨時表的欄位資料,並且把欄位值放入臨時表flow_tmp裡面*/ WHILE i>0 DO SET v_exe_sql=CONCAT('INSERT INTO test.flow_tmp(Number,City,wm_str,Wm) select ',v_Number,',\'',p_city, '\',\'',v_column_name,'\',',v_column_name,' from test.flow_table WHERE flow_table.Number=',v_Number,';'); SET @sql=v_exe_sql; PREPARE s1 FROM @sql; EXECUTE s1; DEALLOCATE PREPARE s1; DELETE FROM test.flow_n_columns WHERE column_name=v_column_name; SELECT column_name INTO v_column_name FROM test.flow_n_columns LIMIT 1; SELECT COUNT(1) INTO i FROM test.flow_n_columns ; DELETE FROM test.flow_tmp WHERE Wm=0; END WHILE; /*由於觸發器是不支援動態sql,所以不能使用while迴圈,動態遍歷所有統計列的,只能寫死列了,如下所示: 現在一個個insert只能寫死了, flow_table表有多少個統計列就寫多少個insert sql,以後新新增一個列,就在這裡新新增一條insertsql語句 INSERT INTO flow_tmp(Number,City,wm_str,Wm) SELECT v_Number,p_city,'wm201403',wm201403 FROM flow_table WHERE Number=v_Number ; INSERT INTO flow_tmp(Number,City,wm_str,Wm) SELECT v_Number,p_city,'wm201404',wm201404 FROM flow_table WHERE Number=v_Number ; INSERT INTO flow_tmp(Number,City,wm_str,Wm) SELECT v_Number,p_city,'wm201405',wm201405 FROM flow_table WHERE Number=v_Number ; INSERT INTO flow_tmp(Number,City,wm_str,Wm) SELECT v_Number,p_city,'wm201406',wm201406 FROM flow_table WHERE Number=v_Number ; INSERT INTO flow_tmp(Number,City,wm_str,Wm) SELECT v_Number,p_city,'wm201407',wm201407 FROM flow_table WHERE Number=v_Number ; INSERT INTO flow_tmp(Number,City,wm_str,Wm) SELECT v_Number,p_city,'wm201408',wm201408 FROM flow_table WHERE Number=v_Number ; */ /*清除掉不資料=0的列*/ DELETE FROM test.flow_tmp WHERE Wm=0 OR Wm IS NULL; SELECT wm_str INTO v_start_wm FROM test.flow_tmp WHERE Number=v_Number ORDER BY wm_str ASC LIMIT 1; SELECT wm_str INTO v_end_wm FROM test.flow_tmp WHERE Number=v_Number ORDER BY wm_str DESC LIMIT 1; SELECT SUM(Wm) INTO v_num FROM test.flow_tmp WHERE Number=v_Number; END
4,列變行結果展示
臨時表的所有資料:
mysql> SELECT * FROM flow_tmp;
+--------+----------+----------+--------+
| Number | City | wm_str | Wm |
+--------+----------+----------+--------+
| 1 | shanghai | wm201403 | 100.20 |
| 1 | shanghai | wm201404 | 180.40 |
| 1 | shanghai | wm201405 | 141.00 |
| 1 | shanghai | wm201406 | 164.00 |
| 1 | shanghai | wm201407 | 124.00 |
| 1 | shanghai | wm201408 | 127.00 |
| 2 | shanghai | wm201403 | 110.23 |
| 2 | shanghai | wm201404 | 180.34 |
| 2 | shanghai | wm201405 | 141.23 |
| 2 | shanghai | wm201406 | 104.78 |
| 2 | shanghai | wm201407 | 124.67 |
| 2 | shanghai | wm201408 | 127.45 |
| 3 | beijing | wm201403 | 123.23 |
| 3 | beijing | wm201404 | 110.34 |
| 3 | beijing | wm201405 | 131.33 |
| 3 | beijing | wm201406 | 154.58 |
| 3 | beijing | wm201407 | 154.67 |
| 3 | beijing | wm201408 | 167.45 |
| 4 | hangzhou | wm201404 | 110.34 |
| 4 | hangzhou | wm201405 | 131.33 |
| 4 | hangzhou | wm201406 | 154.58 |
| 4 | hangzhou | wm201407 | 154.67 |
| 5 | hangzhou | wm201405 | 131.33 |
| 5 | hangzhou | wm201406 | 154.58 |
| 5 | hangzhou | wm201407 | 154.67 |
+--------+----------+----------+--------+
25 rows in set (0.00 sec)
統計每個使用者的使用總量為:
mysql> SELECT Number,SUM(Wm) 'total_num' FROM flow_tmp GROUP BY Number ORDER BY Number;
+--------+-----------+
| Number | total_num |
+--------+-----------+
| 1 | 836.60 |
| 2 | 788.70 |
| 3 | 841.60 |
| 4 | 550.92 |
| 5 | 440.58 |
+--------+-----------+
5 rows in set (0.00 sec)
二,行變列例子演示
1,準備測試資料
DROP TABLE IF EXISTS tb;
CREATE TABLE tb(`cname` VARCHAR(10),cource VARCHAR(10),score INT) ENGINE=INNODB;
INSERT INTO tb VALUES('張三','語文',74);
INSERT INTO tb VALUES('張三','數學',83);
INSERT INTO tb VALUES('張三','物理',93);
INSERT INTO tb VALUES('李四','語文',74);
INSERT INTO tb VALUES('李四','數學',84);
INSERT INTO tb VALUES('李四','物理',94);
SELECT * FROM tb;
需要得到的結果是:
2,利用SUM(IF()) 生成列 + WITH ROLLUP 生成彙總行,並利用 IFNULL將彙總行標題顯示為 Total_num
SQL程式碼塊如下:
SELECT cname AS "姓名",
SUM(IF(cource="語文",score,0)) AS "語文",
SUM(IF(cource="數學",score,0)) AS "數學",
SUM(IF(cource="物理",score,0)) AS "物理",
SUM(score) AS "總成績",
ROUND(AVG(score),2) AS "平均成績"
FROM tb
GROUP BY cname
UNION ALL
SELECT
"總成績平均數",
ROUND(AVG(`語文`),2) , ROUND(AVG(`數學`),2), ROUND(AVG(`物理`),2), ROUND(AVG(`總成績`),2), ROUND(AVG(`平均成績`),2)
FROM(
SELECT "all",cname AS "姓名",
SUM(IF(cource="語文",score,0)) AS "語文",
SUM(IF(cource="數學",score,0)) AS "數學",
SUM(IF(cource="物理",score,0)) AS "物理",
SUM(score) AS "總成績",
AVG(score) AS "平均成績"
FROM tb
GROUP BY cname
)tb2
GROUP BY tb2.all;
執行結果正確,如下所示:
3,利用max(CASE ... WHEN ... THEN .. ELSE END) AS "語文"的方式來實現
SQL程式碼如下:
SELECT
cname AS "姓名",
MAX(CASE cource WHEN "語文" THEN score ELSE 0 END) AS "語文",
MAX(CASE cource WHEN "數學" THEN score ELSE 0 END) AS "數學",
MAX(CASE cource WHEN "物理" THEN score ELSE 0 END) AS "物理",
SUM(score) AS "總成績",
ROUND(AVG(score) ,2) AS "平均成績"
FROM tb
GROUP BY `cname`
UNION ALL
SELECT
"總成績平均數",
ROUND(AVG(`語文`),2) , ROUND(AVG(`數學`),2), ROUND(AVG(`物理`),2), ROUND(AVG(`總成績`),2), ROUND(AVG(`平均成績`),2)
FROM( SELECT 'all' ,
cname AS "姓名",
MAX(CASE cource WHEN "語文" THEN score ELSE 0 END) AS "語文",
MAX(CASE cource WHEN "數學" THEN score ELSE 0 END) AS "數學",
MAX(CASE cource WHEN "物理" THEN score ELSE 0 END) AS "物理",
SUM(score) AS "總成績",
ROUND(AVG(score) ,2) AS "平均成績"
FROM tb
GROUP BY `cname`
)tb2 GROUP BY tb2.all
執行結果正確,如下所示:
4,利用 WITH rollup結果不符合
SQL程式碼如下:SELECT IFNULL(cname,'總平均數') AS "姓名",
MAX(CASE cource WHEN "語文" THEN score ELSE 0 END) AS "語文",
MAX(CASE cource WHEN "數學" THEN score ELSE 0 END) AS "數學",
MAX(CASE cource WHEN "物理" THEN score ELSE 0 END) AS "物理",
ROUND(AVG(score),2) AS "總成績",
ROUND(AVG(avg_score),2) AS "平均成績"
FROM(
SELECT
cname ,
IFNULL(cource,'total') cource,
SUM(score) AS score,
ROUND(AVG(score) ,2) AS avg_score
FROM tb
GROUP BY `cname`,cource WITH ROLLUP HAVING cname IS NOT NULL
)tb2
GROUP BY tb2.cname WITH ROLLUP;
執行結果不對,總平均數欄目,裡面各科比較語文數學的班級平均數不對,如下所示:
mysql> SELECT IFNULL(cname,'總平均數') AS "姓名",
-> MAX(CASE cource WHEN "語文" THEN score ELSE 0 END) AS "語文",
-> MAX(CASE cource WHEN "數學" THEN score ELSE 0 END) AS "數學",
-> MAX(CASE cource WHEN "物理" THEN score ELSE 0 END) AS "物理",
-> ROUND(AVG(score),2) AS "總成績",
-> ROUND(AVG(avg_score),2) AS "平均成績"
-> FROM(
-> SELECT
->
Display ALL 793 possibilities? (Y OR n)
-> cname ,
->
Display ALL 793 possibilities? (Y OR n)
-> IFNULL(cource,'total') cource,
->
Display ALL 793 possibilities? (Y OR n)
-> SUM(score) AS score,
->
Display ALL 793 possibilities? (Y OR n)
-> ROUND(AVG(score) ,2) AS avg_score
-> FROM tb
-> GROUP BY `cname`,cource WITH ROLLUP HAVING cname IS NOT NULL
-> )tb2
-> GROUP BY tb2.cname WITH ROLLUP;
+--------------+--------+--------+--------+-----------+--------------+
| 姓名 | 語文 | 數學 | 物理 | 總成績 | 平均成績 |
+--------------+--------+--------+--------+-----------+--------------+
| 張三 | 74 | 83 | 93 | 125.00 | 83.33 |
| 李四 | 74 | 84 | 94 | 126.00 | 84.00 |
| 總平均數 | 74 | 84 | 94 | 125.50 | 83.67 |
+--------------+--------+--------+--------+-----------+--------------+
3 ROWS IN SET, 1 warning (0.00 sec)
總結: WITH rollup中對求列的總數是OK的,但是求列的平均數有偏差,這裡場景使用不是恰當。
5,使用動態SQL來實現
SQL程式碼塊如下:
/*僅僅班級成員部分*/
SET @a='';
SELECT @a:=CONCAT(@a,'SUM(IF(cource=\'',cource,'\'',',score,0)) AS ',cource,',') FROM (SELECT DISTINCT cource FROM tb) A;
SET @a=CONCAT(@a,"ROUND(AVG(score) ,2) AS \"平均成績\"");
SET @b=CONCAT('SELECT IFNULL(cname,\'總成績\'),',LEFT(@a,LENGTH(@a)-1),' ,SUM(score) AS \"總成績\" FROM tb GROUP BY cname ');
/*班級成員總計部分**/
SET @a2="";
SET @b2=CONCAT('SELECT "all",IFNULL(cname,\'總成績\'),',LEFT(@a,LENGTH(@a)-1),' ,SUM(score) AS \"總成績\" FROM tb GROUP BY cname ');
SELECT @a2:=CONCAT(@a2,'ROUND(AVG(`',cource,'`),2),') FROM (SELECT DISTINCT cource FROM tb) A;
SET @a2=CONCAT(@a2," ROUND(AVG(`平均成績`),2),ROUND(AVG(`總成績`),2) ");
SET @c=CONCAT("SELECT \"班級平均數\",",LEFT(@a2,LENGTH(@a)-1)," FROM(",@b2,")tb2 GROUP BY tb2.all;");
SET @d=CONCAT(@b," UNION ALL ",@c);
PREPARE stmt1 FROM @d;
EXECUTE stmt1;
檢視執行結果如下,已經達到效果:
mysql> /*僅僅班級成員部分*/
mysql> SET @a='';
QUERY OK, 0 ROWS affected (0.00 sec)
mysql> SELECT @a:=CONCAT(@a,'SUM(IF(cource=\'',cource,'\'',',score,0)) AS ',cource,',') FROM (SELECT DISTINCT cource FROM tb) A;
+-----------------------------------------------------------------------------------------------------------------------------------+
| @a:=CONCAT(@a,'SUM(IF(cource=\'',cource,'\'',',score,0)) AS ',cource,',') |
+-----------------------------------------------------------------------------------------------------------------------------------+
| SUM(IF(cource='語文',score,0)) AS 語文, |
| SUM(IF(cource='語文',score,0)) AS 語文,SUM(IF(cource='數學',score,0)) AS 數學, |
| SUM(IF(cource='語文',score,0)) AS 語文,SUM(IF(cource='數學',score,0)) AS 數學,SUM(IF(cource='物理',score,0)) AS 物理, |
+-----------------------------------------------------------------------------------------------------------------------------------+
3 ROWS IN SET (0.00 sec)
mysql> SET @a=CONCAT(@a,"ROUND(AVG(score) ,2) AS \"平均成績\"");
QUERY OK, 0 ROWS affected (0.00 sec)
mysql> SET @b=CONCAT('SELECT IFNULL(cname,\'總成績\'),',LEFT(@a,LENGTH(@a)-1),' ,SUM(score) AS \"總成績\" FROM tb GROUP BY cname ');
QUERY OK, 0 ROWS affected (0.00 sec)
mysql>
mysql> /*班級成員總計部分**/
mysql> SET @a2="";
QUERY OK, 0 ROWS affected (0.00 sec)
mysql> SET @b2=CONCAT('SELECT "all",IFNULL(cname,\'總成績\'),',LEFT(@a,LENGTH(@a)-1),' ,SUM(score) AS \"總成績\" FROM tb GROUP BY cname ');
QUERY OK, 0 ROWS affected (0.00 sec)
mysql> SELECT @a2:=CONCAT(@a2,'ROUND(AVG(`',cource,'`),2),') FROM (SELECT DISTINCT cource FROM tb) A;
+-----------------------------------------------------------------------+
| @a2:=CONCAT(@a2,'ROUND(AVG(`',cource,'`),2),') |
+-----------------------------------------------------------------------+
| ROUND(AVG(`語文`),2), |
| ROUND(AVG(`語文`),2),ROUND(AVG(`數學`),2), |
| ROUND(AVG(`語文`),2),ROUND(AVG(`數學`),2),ROUND(AVG(`物理`),2), |
+-----------------------------------------------------------------------+
3 ROWS IN SET (0.00 sec)
mysql> SET @a2=CONCAT(@a2," ROUND(AVG(`平均成績`),2),ROUND(AVG(`總成績`),2) ");
QUERY OK, 0 ROWS affected (0.00 sec)
mysql> SET @c=CONCAT("SELECT \"班級平均數\",",LEFT(@a2,LENGTH(@a)-1)," FROM(",@b2,")tb2 GROUP BY tb2.all;");
QUERY OK, 0 ROWS affected (0.00 sec)
mysql> SET @d=CONCAT(@b," UNION ALL ",@c);
QUERY OK, 0 ROWS affected (0.00 sec)
mysql>
mysql> PREPARE stmt1 FROM @d;
QUERY OK, 0 ROWS affected (0.00 sec)
Statement prepared
mysql> EXECUTE stmt1;
+---------------------------+--------+--------+--------+--------------+-----------+
| IFNULL(cname,'總成績') | 語文 | 數學 | 物理 | 平均成績 | 總成績 |
+---------------------------+--------+--------+--------+--------------+-----------+
| 張三 | 74.00 | 83.00 | 93.00 | 83.33 | 250.00 |
| 李四 | 74.00 | 84.00 | 94.00 | 84.00 | 252.00 |
| 班級平均數 | 74.00 | 83.50 | 93.50 | 83.67 | 251.00 |
+---------------------------+--------+--------+--------+--------------+-----------+
3 ROWS IN SET (0.00 sec)