数据库系统原理与应用教程(052)—— MySQL 的数据完整性(十四):交叉表查询(行列转换)
目录
- 数据库系统原理与应用教程(052)—— MySQL 的数据完整性(十四):交叉表查询(行列转换)
- 一、交叉表的形式
- 二、静态交叉表
- 三、动态交叉表
- 1、取出要进行行列转换的列中的数据
- 2、使用 CONCAT 函数生成拼接字符串
- 3、拼接完整的 SELECT 语句
- 4、使用预处理命令执行拼接好的字符串
- 5、把交叉表查询定义为存储过程
交叉表查询是将表中的列进行分组,一组列在交叉表左侧,一组列在交叉表上部,并在交叉表行与列交叉处显示表中某个字段的各种计算值。
交叉表分为静态交叉表和动态交叉表。静态交叉表中的列是固定的,而且要提前确定要进行行列转换的列中的数据内容,在使用中灵活性不够。动态交叉表中的列根据要进行行列转换的列中的数据动态生成,不用提前确定该列的数据内容,甚至不用知道该列的数据是什么。
使用交叉表查询来计算和重构数据,可以简化数据分析。交叉表查询计算数据的和,平均值,计数及其他类型的统计,并将它们分组,一组列在数据表左侧作为交叉表的行字段,另一组列在数据表的顶端作为交叉表的列字段。
一、交叉表的形式
交叉表查询又称为行列转换,就是把下面的查询结果转换为表1(交叉表)。
/*
select s.s_id, s.s_name, c.c_id, c.c_name, sc.score
from student s,course c,score sc where s.s_id=sc.s_id and c.c_id=sc.c_id;
*/
mysql> select s.s_id, s.s_name, c.c_id, c.c_name, sc.score
-> from student s,course c,score sc where s.s_id=sc.s_id and c.c_id=sc.c_id;
+-------+-----------+------+--------------+-------+
| s_id | s_name | c_id | c_name | score |
+-------+-----------+------+--------------+-------+
| S2011 | 张晓刚 | C102 | 高等数学 | 84 |
| S2011 | 张晓刚 | C105 | 传染病学 | 90 |
| S2012 | 刘小青 | C101 | 古代文学 | 67 |
| S2012 | 刘小青 | C102 | 高等数学 | 52 |
| S2012 | 刘小青 | C103 | 线性代数 | 55 |
| S2012 | 刘小青 | C104 | 临床医学 | 86 |
| S2012 | 刘小青 | C105 | 传染病学 | 87 |
| S2013 | 曹梦德 | C102 | 高等数学 | 97 |
| S2013 | 曹梦德 | C103 | 线性代数 | 68 |
| S2013 | 曹梦德 | C104 | 临床医学 | 66 |
| S2013 | 曹梦德 | C105 | 传染病学 | 68 |
| S2014 | 刘艳 | C102 | 高等数学 | 90 |
| S2014 | 刘艳 | C103 | 线性代数 | 85 |
| S2014 | 刘艳 | C104 | 临床医学 | 77 |
| S2014 | 刘艳 | C105 | 传染病学 | 96 |
| S2015 | 刘艳 | C101 | 古代文学 | 69 |
| S2015 | 刘艳 | C102 | 高等数学 | 66 |
| S2015 | 刘艳 | C103 | 线性代数 | 88 |
| S2015 | 刘艳 | C104 | 临床医学 | 69 |
| S2015 | 刘艳 | C105 | 传染病学 | 66 |
| S2016 | 刘若非 | C101 | 古代文学 | 65 |
| S2016 | 刘若非 | C102 | 高等数学 | 69 |
| S2021 | 董雯花 | C102 | 高等数学 | 72 |
| S2021 | 董雯花 | C103 | 线性代数 | 90 |
| S2021 | 董雯花 | C104 | 临床医学 | 90 |
| S2021 | 董雯花 | C105 | 传染病学 | 57 |
| S2022 | 周华建 | C102 | 高等数学 | 88 |
| S2022 | 周华建 | C103 | 线性代数 | 93 |
| S2023 | 特朗普 | C102 | 高等数学 | 68 |
| S2023 | 特朗普 | C103 | 线性代数 | 86 |
| S2024 | 奥巴马 | C102 | 高等数学 | 87 |
| S2024 | 奥巴马 | C103 | 线性代数 | 97 |
| S2025 | 周健华 | C102 | 高等数学 | 61 |
| S2025 | 周健华 | C105 | 传染病学 | 62 |
| S2026 | 张学有 | C102 | 高等数学 | 59 |
| S2026 | 张学有 | C105 | 传染病学 | 48 |
+-------+-----------+------+--------------+-------+
36 rows in set (0.00 sec)
-- 表1(交叉表)
+-----------+------------+------------+------------+-------------+-------------+--------+
| s_name | 古代文学 | 高等数学 | 线性代数 | 临床医学 | 传染病学 | 总分 |
+-----------+------------+------------+------------+-------------+-------------+--------+
| 刘小青 | 67 | 52 | 55 | 86 | 87 | 347 |
| 刘艳 | 69 | 156 | 173 | 146 | 16 | 706 |
| 刘若非 | 65 | 69 | 未选修 | 未选修 | 未选修 | 134 |
| 周健华 | 未选修 | 61 | 未选修 | 未选修 | 62 | 123 |
| 周华建 | 未选修 | 88 | 93 | 未选修 | 未选修 | 181 |
| 奥巴马 | 未选修 | 87 | 97 | 未选修 | 未选修 | 184 |
| 张学有 | 未选修 | 59 | 未选修 | 未选修 | 48 | 107 |
| 张晓刚 | 未选修 | 84 | 未选修 | 未选修 | 90 | 174 |
| 曹梦德 | 未选修 | 97 | 68 | 66 | 68 | 299 |
| 特朗普 | 未选修 | 68 | 86 | 未选修 | 未选修 | 154 |
| 董雯花 | 未选修 | 72 | 90 | 90 | 57 | 309 |
+-----------+-------------+-----------+-------------+-------------+-------------+--------+
二、静态交叉表
生成交叉表的基本方法为:
(1)按照交叉表的行字段进行分组(比如 s_name)。
(2)对要转换为列的行字段(比如 c_name)进行统计,在统计函数中使用 IF 函数对数据进行筛选,生成列字段。
交叉表查询代码如下:
/*
select ifnull(s_name,'总分') 姓名,
if(sum(if(c.c_id='C101',score,0))=0,'未选修',sum(if(c.c_id='C101',score,0))) 古代文学,
if(sum(if(c.c_id='C102',score,0))=0,'未选修',sum(if(c.c_id='C102',score,0))) 高等数学,
if(sum(if(c.c_id='C103',score,0))=0,'未选修',sum(if(c.c_id='C103',score,0))) 线性代数,
if(sum(if(c.c_id='C104',score,0))=0,'未选修',sum(if(c.c_id='C104',score,0))) 临床医学,
if(sum(if(c.c_id='C105',score,0))=0,'未选修',sum(if(c.c_id='C105',score,0))) 传染病学,
sum(score) 总分
from student s,course c,score sc where s.s_id=sc.s_id and c.c_id=sc.c_id
group by s_name with rollup;
*/
mysql> select ifnull(s_name,'总分') 姓名,
-> if(sum(if(c.c_id='C101',score,0))=0,'未选修',sum(if(c.c_id='C101',score,0))) 古代文学,
-> if(sum(if(c.c_id='C102',score,0))=0,'未选修',sum(if(c.c_id='C102',score,0))) 高等数学,
-> if(sum(if(c.c_id='C103',score,0))=0,'未选修',sum(if(c.c_id='C103',score,0))) 线性代数,
-> if(sum(if(c.c_id='C104',score,0))=0,'未选修',sum(if(c.c_id='C104',score,0))) 临床医学,
-> if(sum(if(c.c_id='C105',score,0))=0,'未选修',sum(if(c.c_id='C105',score,0))) 传染病学,
-> sum(score) 总分
-> from student s,course c,score sc where s.s_id=sc.s_id and c.c_id=sc.c_id
-> group by s_name with rollup;
+-----------+-------------+-------------+-------------+-------------+-------------+--------+
| 姓名 | 古代文学 | 高等数学 | 线性代数 | 临床医学 | 传染病学 | 总分 |
+-----------+-------------+-------------+-------------+-------------+-------------+--------+
| 刘小青 | 67 | 52 | 55 | 86 | 87 | 347 |
| 刘艳 | 69 | 156 | 173 | 146 | 162 | 706 |
| 刘若非 | 65 | 69 | 未选修 | 未选修 | 未选修 | 134 |
| 周健华 | 未选修 | 61 | 未选修 | 未选修 | 62 | 123 |
| 周华建 | 未选修 | 88 | 93 | 未选修 | 未选修 | 181 |
| 奥巴马 | 未选修 | 87 | 97 | 未选修 | 未选修 | 184 |
| 张学有 | 未选修 | 59 | 未选修 | 未选修 | 48 | 107 |
| 张晓刚 | 未选修 | 84 | 未选修 | 未选修 | 90 | 174 |
| 曹梦德 | 未选修 | 97 | 68 | 66 | 68 | 299 |
| 特朗普 | 未选修 | 68 | 86 | 未选修 | 未选修 | 154 |
| 董雯花 | 未选修 | 72 | 90 | 90 | 57 | 309 |
| 总分 | 201 | 893 | 662 | 388 | 574 | 2718 |
+-----------+--------------+-------------+-------------+-------------+-------------+--------+
12 rows in set (0.03 sec)
三、动态交叉表
实现动态交叉表的基本思路为:
(1)使用 GROUP_CONCAT 函数取出要进行行列转换的列中的所有数据。
(2)使用 CONCAT 函数拼接成和静态交叉表查询相同的 SELECT 语句。
(3)使用预处理(prepare)命令执行,得到最终的查询结果。
仍然以上面的 student、course、score 三个表为例,动态生成交叉表。
1、取出要进行行列转换的列中的数据
使用 GROUP_CONCAT 函数取出要进行行列转换的列(c_name)中的数据。
/*
select group_concat(distinct c.c_name)
from student s,course c,score sc where s.s_id=sc.s_id and c.c_id=sc.c_id;
*/
mysql> select group_concat(distinct c.c_name)
-> from student s,course c,score sc where s.s_id=sc.s_id and c.c_id=sc.c_id;
+------------------------------------------------------------------+
| group_concat(distinct c.c_name) |
+------------------------------------------------------------------+
| 临床医学,传染病学,古代文学,线性代数,高等数学 |
+------------------------------------------------------------------+
1 row in set (0.00 sec)
2、使用 CONCAT 函数生成拼接字符串
/*
说明:为 \' 为转义字符,把单引号(')拼接到字符串中
SELECT GROUP_CONCAT(DISTINCT
(CONCAT('IF(SUM(IF(c_name=\'',c_name,'\',score,0))=0,\'未选修\',SUM(IF(c_name=\'',c_name,'\',score,0))) ',c_name))
)
INTO @sql_str
FROM student s,course c,score sc, (SELECT @sql_str) a
where s.s_id=sc.s_id and c.c_id=sc.c_id;
*/
mysql> SELECT GROUP_CONCAT(DISTINCT
-> (CONCAT('IF(SUM(IF(c_name=\'',c_name,'\',score,0))=0,\'未选修\',SUM(IF(c_name=\'',c_name,'\',score,0))) ',c_name))
-> )
-> INTO @sql_str
-> FROM student s,course c,score sc, (SELECT @sql_str) a
-> where s.s_id=sc.s_id and c.c_id=sc.c_id;
Query OK, 1 row affected (0.00 sec)
mysql> SELECT @sql_str;
+-----------------------------------------------------------+|
@sql_str
|+----------------------------------------------------------+|
IF(SUM(IF(c_name='临床医学',score,0))=0,'未选修',SUM(IF(c_name='临床医学',score,0))) 临床医学,
IF(SUM(IF(c_name='传染病学',score,0))=0,'未选修',SUM(IF(c_name='传染病学',score,0))) 传染病学,
IF(SUM(IF(c_name='古代文学',score,0))=0,'未选修',SUM(IF(c_name='古代文学',score,0))) 古代文学,
IF(SUM(IF(c_name='线性代数',score,0))=0,'未选修',SUM(IF(c_name='线性代数',score,0))) 线性代数,
IF(SUM(IF(c_name='高等数学',score,0))=0,'未选修',SUM(IF(c_name='高等数学',score,0))) 高等数学
|+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
3、拼接完整的 SELECT 语句
/*
SELECT GROUP_CONCAT(DISTINCT
(CONCAT('IF(SUM(IF(c_name=\'',c_name,'\',score,0))=0,\'未选修\',SUM(IF(c_name=\'',c_name,'\',score,0))) ',c_name))
)
INTO @sql_str
FROM student s,course c,score sc, (SELECT @sql_str) a
where s.s_id=sc.s_id and c.c_id=sc.c_id;
set @sql_str =
CONCAT('SELECT IFNULL(s_name, \'总分\') 姓名,',
@sql_str,
',SUM(score) 总分
FROM student s,course c,score sc
where s.s_id=sc.s_id and c.c_id=sc.c_id
GROUP BY s.s_name WITH ROLLUP;');
*/
mysql> SELECT GROUP_CONCAT(DISTINCT
-> (CONCAT('IF(SUM(IF(c_name=\'',c_name,'\',score,0))=0,\'未选修\',SUM(IF(c_name=\'',c_name,'\',score,0))) ',c_name))
-> )
-> INTO @sql_str
-> FROM student s,course c,score sc, (SELECT @sql_str) a
-> where s.s_id=sc.s_id and c.c_id=sc.c_id;
Query OK, 1 row affected (0.00 sec)
mysql> set @sql_str =
-> CONCAT('SELECT IFNULL(s_name, \'总分\') 姓名,',
-> @sql_str,
-> ',SUM(score) 总分
'> FROM student s,course c,score sc
'> where s.s_id=sc.s_id and c.c_id=sc.c_id
'> GROUP BY s.s_name WITH ROLLUP;');
Query OK, 0 rows affected (0.00 sec)'
mysql> select @sql_str;
+---------------------------------------------------------------+|
@sql_str
|+-------------------------------------------------------------+|
SELECT
IFNULL(s_name, '总分') 姓名,
IF(SUM(IF(c_name='临床医学',score,0))=0,'未选修',SUM(IF(c_name='临床医学',score,0))) 临床医学,
IF(SUM(IF(c_name='传染病学',score,0))=0,'未选修',SUM(IF(c_name='传染病学',score,0))) 传染病学,
IF(SUM(IF(c_name='古代文学',score,0))=0,'未选修',SUM(IF(c_name='古代文学',score,0))) 古代文学,
IF(SUM(IF(c_name='线性代数',score,0))=0,'未选修',SUM(IF(c_name='线性代数',score,0))) 线性代数,
IF(SUM(IF(c_name='高等数学',score,0))=0,'未选修',SUM(IF(c_name='高等数学',score,0))) 高等数学,
SUM(score) 总分
FROM student s,course c,score sc
where s.s_id=sc.s_id and c.c_id=sc.c_id
GROUP BY s.s_name WITH ROLLUP;
+---------------------------------------------------------------------------+
1 row in set (0.00 sec)
4、使用预处理命令执行拼接好的字符串
/*
-- PREPARE 语句语法说明:
PREPARE statement_name FROM SQL字符串;
-- 执行预处理语句
EXECUTE statement_name;
-- 删除预处理语句定义
DEALLOCATE | DROP PREPARE statement_name;
*/
mysql> PREPARE stmt from @sql_str;
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> EXECUTE stmt;
+-----------+-------------+-------------+-------------+--------------+-------------+--------+
| 姓名 | 临床医学 | 传染病学 | 古代文学 | 线性代数 | 高等数学 | 总分 |
+-----------+-------------+-------------+-------------+--------------+-------------+--------+
| 刘小青 | 86 | 87 | 67 | 55 | 52 | 347 |
| 刘艳 | 146 | 162 | 69 | 173 | 156 | 706 |
| 刘若非 | 未选修 | 未选修 | 65 | 未选修 | 69 | 134 |
| 周健华 | 未选修 | 62 | 未选修 | 未选修 | 61 | 123 |
| 周华建 | 未选修 | 未选修 | 未选修 | 93 | 88 | 181 |
| 奥巴马 | 未选修 | 未选修 | 未选修 | 97 | 87 | 184 |
| 张学有 | 未选修 | 48 | 未选修 | 未选修 | 59 | 107 |
| 张晓刚 | 未选修 | 90 | 未选修 | 未选修 | 84 | 174 |
| 曹梦德 | 66 | 68 | 未选修 | 68 | 97 | 299 |
| 特朗普 | 未选修 | 未选修 | 未选修 | 86 | 68 | 154 |
| 董雯花 | 90 | 57 | 未选修 | 90 | 72 | 309 |
| 总分 | 388 | 574 | 201 | 662 | 893 | 2718 |
+-----------+-------------+--------------+-------------+--------------+-------------+--------+
12 rows in set (0.00 sec)
mysql> DEALLOCATE prepare stmt;
Query OK, 0 rows affected (0.00 sec)
5、把交叉表查询定义为存储过程
把上述代码定义为一个存储过程保存到数据库中,以实现重复调用。
代码如下:
DELIMITER &&
CREATE PROCEDURE sp_crosstable() reads SQL data
BEGIN
SET @sql_str = NULL;
SELECT GROUP_CONCAT(DISTINCT
(CONCAT('IF(SUM(IF(c_name=\'',c_name,'\',score,0))=0,\'未选修\',SUM(IF(c_name=\'',c_name,'\',score,0))) ',c_name))
)
INTO @sql_str
FROM student s,course c,score sc, (SELECT @sql_str) a
where s.s_id=sc.s_id and c.c_id=sc.c_id;
set @sql_str =
CONCAT('SELECT IFNULL(s_name, \'总分\') 姓名,',
@sql_str,
',SUM(score) 总分
FROM student s,course c,score sc
where s.s_id=sc.s_id and c.c_id=sc.c_id
GROUP BY s.s_name WITH ROLLUP;');
PREPARE stmt from @sql_str;
EXECUTE stmt;
DEALLOCATE prepare stmt;
END &&
DELIMITER ;
执行以上代码,生成存储过程:
mysql> DELIMITER &&
mysql> CREATE PROCEDURE sp_crosstable() reads SQL data
-> BEGIN
-> SET @sql_str = NULL;
->
-> SELECT GROUP_CONCAT(DISTINCT
-> (CONCAT('IF(SUM(IF(c_name=\'',c_name,'\',score,0))=0,\'未选修\',SUM(IF(c_name=\'',c_name,'\',score,0))) ',c_name))
-> )
-> INTO @sql_str
-> FROM student s,course c,score sc, (SELECT @sql_str) a
-> where s.s_id=sc.s_id and c.c_id=sc.c_id;
->
-> set @sql_str =
-> CONCAT('SELECT IFNULL(s_name, \'总分\') 姓名,',
-> @sql_str,
-> ',SUM(score) 总分
'> FROM student s,course c,score sc
'> where s.s_id=sc.s_id and c.c_id=sc.c_id
'> GROUP BY s.s_name WITH ROLLUP;');
->
-> PREPARE stmt from @sql_str;
-> EXECUTE stmt;
-> DEALLOCATE prepare stmt;
-> END &&
Query OK, 0 rows affected (0.10 sec)
mysql> DELIMITER ;
mysql> select name from mysql.proc where db='mydb';
+---------------+
| name |
+---------------+
| sp_crosstable |
+---------------+
1 row in set (0.01 sec)
执行存储过程,生成交叉表:
mysql> CALL sp_crosstable;
+-----------+--------------+--------------+--------------+--------------+--------------+--------+
| 姓名 | 临床医学 | 传染病学 | 古代文学 | 线性代数 | 高等数学 | 总分 |
+-----------+--------------+--------------+--------------+--------------+--------------+--------+
| 刘小青 | 86 | 87 | 67 | 55 | 52 | 347 |
| 刘艳 | 146 | 162 | 69 | 173 | 156 | 706 |
| 刘若非 | 未选修 | 未选修 | 65 | 未选修 | 69 | 134 |
| 周健华 | 未选修 | 62 | 未选修 | 未选修 | 61 | 123 |
| 周华建 | 未选修 | 未选修 | 未选修 | 93 | 88 | 181 |
| 奥巴马 | 未选修 | 未选修 | 未选修 | 97 | 87 | 184 |
| 张学有 | 未选修 | 48 | 未选修 | 未选修 | 59 | 107 |
| 张晓刚 | 未选修 | 90 | 未选修 | 未选修 | 84 | 174 |
| 曹梦德 | 66 | 68 | 未选修 | 68 | 97 | 299 |
| 特朗普 | 未选修 | 未选修 | 未选修 | 86 | 68 | 154 |
| 董雯花 | 90 | 57 | 未选修 | 90 | 72 | 309 |
| 总分 | 388 | 574 | 201 | 662 | 893 | 2718 |
+-----------+--------------+--------------+--------------+--------------+--------------+--------+
12 rows in set (0.03 sec)
Query OK, 0 rows affected (0.03 sec)