8. 多表查询(连接)

/*
在实际查询中,很多情况下用户需要的数据并不全在一个表中,而是存在于多个不同的表,此时就要使用多表查询,多表查询是通过各个表之间的共同列的相关性来查询数据的,多表查询要在这些表中建立连接,再在连接生成的结果基础上进行筛选。
在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:
一对多(多对一)        班级 与 学生的关系
多对多                学生 与 课程的关系
一对一                学生 与 学生详情的关系

*/
/*
笛卡尔乘积现象:表1 有m行,表2有n行,结果=m*n行

发生原因:没有有效的连接条件
如何避免:添加有效的连接条件
*/
sql
SELECT * from teacher,course; -- 错误的写法
SELECT * from teacher,course WHERE teacher.tno=course.tno; -- 正确的写法
#1、等值连接
/*

① 多表等值连接的结果为多表的交集部分
② n表连接,至少需要n-1个连接条件
③ 多表的顺序没有要求
④ 一般需要为表起别名
⑤ 可以搭配前面介绍的所有子句使用,比如排序、分组、筛选
*/
-- 列出所有老师任教的课程
sql
SELECT * from teacher,course WHERE teacher.tno=course.tno ORDER BY cno;
-- 列出各位老师任教的课程成绩
sql
SELECT tname,cname,score from teacher,course,sc where teacher.tno=course.tno and course.cno=sc.cno;
-- 统计各位老师任教的学生人数
sql
SELECT tname,count(*) 学生人数 from teacher,course,sc where teacher.tno=course.tno and course.cno=sc.cno GROUP BY tname;
-- 列出李老师任教的学生的姓名,成绩;
sql
select tname,sname,score from teacher,course,sc,student where teacher.tno=course.tno and course.cno=sc.cno and sc.sno=student.sno and teacher.tname="李老师";
-- 求李老师任课的课程平均成绩
sql
SELECT tname,avg(score) as "平均成绩" from teacher,course,sc where teacher.tno=course.tno and course.cno=sc.cno and tname="李老师";
-- 2、非等值连接

-- 查询所有学生各科的等级
sql
SELECT sc.sno,student.sname,course.cname,grades.grade_level 
from sc,grades,student,course
WHERE sc.sno=student.sno and course.cno=sc.cno and sc.score BETWEEN grades.lowest_sc and grades.highest_sc;
/*
多表查询语法格式如下:
SELECT [表名.] 目标字段表达式 [AS别名],...
FROM 左表名 [AS别名] 连接类型 右表名 [AS别名]
ON 连接条件
[WHERE 条件表达式];
其中,连接类型以及运算符共有5种。

(1)CROSS JOIN:交叉连接 笛卡尔积。
(2)INNER JOIN或JOIN:内连接。
(3)LEFT JOIN或LEFT OUTER JOIN:左外连接。
(4)RIGHT JOIN或RIGHT OUTER JOIN:右外连接。
(5)FULL JOIN或FULL OUTER JOIN:完全连接。

*/
/*
2)内连接
内连接是指用比较运算符设置连接条件,只返回满足连接条件的数据行,是将交叉连接生成的结果集按照连接条件进行筛选后形成的。内连接有以下两种语法格式:
SELECT 字段名列表
FROM 表名1 [INNER] JOIN 表名2
ON 表名1.字段名 比较运算符 表名2.字段名;
或者
SELECT 字段名列表
FROM 表名1,表名2
WHERE 表名1.字段名 比较运算符 表名2.字段名;

*/
--
-- 列出所有老师任教的课程
sql
SELECT * from teacher 
inner join course on teacher.tno=course.tno 
ORDER BY cno;
-- 统计各位老师任教的学生人数
sql
select tname,count(*) 学生人数 
from teacher 
INNER JOIN course on teacher.tno=course.tno
inner join sc on course.cno=sc.cno 
group by tname;
SELECT tname,count(*) 学生人数 from teacher,course,sc where teacher.tno=course.tno and course.cno=sc.cno GROUP BY tname;
-- 列出李老师任教的学生的姓名,成绩;
sql
select teacher.tname,student.sname,sc.score from teacher 
inner join course on teacher.tno = course.tno 
inner join sc on course.cno = sc.cno 
inner join student on sc.sno = student.sno 
where teacher.tname='李老师';
select tname,sname,score from teacher,course,sc,student where teacher.tno=course.tno and course.cno=sc.cno and sc.sno=student.sno and teacher.tname="李老师";
-- 求李老师任课的课程平均成绩(保留两位小数)
sql
select tname,round(avg(score),2) 平均成绩 from teacher 
inner join course on teacher.tno = course.tno 
inner join sc on course.cno=sc.cno 
where teacher.tname='李老师';
SELECT tname,avg(score) as "平均成绩" from teacher,course,sc where teacher.tno=course.tno and course.cno=sc.cno and tname="李老师";
/*
3)外连接
外连接与内连接不同,有主从表之分。使用外连接时,以主表中每行数据去匹配从表中的数据行,如果符合连接条件则返回到结果集中;如果没有找到匹配的数据行,则在结果集中仍然保留主表的数据行,相对应的从表中的字段则被填上NULL值。
外连接的语法格式如下:
SELECT 字段名列表
FROM 表名1 LEFT|RIGHT JOIN 表名2
ON 表名1.字段名 比较运算符 表名2.字段名;

外连接可以分为左外连接、右外连接和全外连接三种类型。
(1)左外连接(Left  Join):
返回左表中的所有记录,以及右表中与左表匹配的记录。如果右表中没有与左表匹配的记录,则右表对应部分为NULL。左外连接使用关键字“LEFT JOIN”来表示。
(2)右外连接(Right Join):
返回右表中的所有记录,以及左表中与右表匹配的记录。如果左表中没有与右表匹配的记录,则左表对应部分为NULL。右外连接使用关键字“RIGHT JOIN”来表示。
(3)全外连接(Full Join):MySQL中无此连接,只能使用union 或union all 将两个联接合并
返回两个表中的所有记录,无论是否满足连接条件。如果某一表中没有与另一表匹配的记录,则该表中的对应部分为NULL。全外连接使用关键字“FULL JOIN”来表示。

*/
-- 找出所有教师的任课情况(包含所有课程,不包含未任课的教师)
sql
SELECT tname,cname from teacher right join course on teacher.tno=course.tno;
select tname,cname from course left join teacher on teacher.tno=course.tno;
-- 不包含未安排任课教师的课程,包含未任课的教师
sql
SELECT tname,cname from teacher LEFT JOIN course on teacher.tno=course.tno;
select tname,cname from course right join teacher on teacher.tno=course.tno;
-- 既不包含未安排任课教师的课程,也不包含未任课的教师
sql
select tname,cname from teacher join course on teacher.tno=course.tno;
-- 既包含未安排任课教师的课程,也包含未任课的教师
sql
SELECT tname,cname from teacher right join course on teacher.tno=course.tno 
union 
SELECT tname,cname from teacher LEFT JOIN course on teacher.tno=course.tno;
-- 将未安排课程的教师,课程显示为未排课
sql
SELECT tname,IFNULL(cname,'未排课') 课程 
from teacher left join course
on teacher.tno=course.tno;
/*
4)自连接 与 子查询
自连接(Self-Join)是指在一个表中进行连接操作,将该表与自身进行关联。自连接常用于对表中的数据进行比较和分析,特别是当表中的某些列关联到同一个表中的其他记录时。
在自连接中,我们需要使用别名来区分被连接的两个表,因为自连接就是一个表的两个副本之间的内连接,即同一个表名在FROM子句中出现两次,必须对表指定不同的别名,字段名前也要加上表的别名进行限定。

*/
-- 查找和赵雷同班级且同籍贯的学生对,显示学生的学号,姓名,籍贯,班级
sql
SELECT a.sno,a.sname,a.sbirthplace,a.class,b.sno,b.sname,b.sbirthplace,b.class 
from student a join student b 
on a.sbirthplace=b.sbirthplace and a.class=b.class 
where a.sname="赵雷" and a.sno<>b.sno;
-- 找出年龄大于所在班级平均年龄的学生
sql
SELECT 
    s.sno, s.sname, s.sage, sub.avg_age
FROM 
    student s
JOIN 
    (
        SELECT class, AVG(sage) AS avg_age
        FROM student
        GROUP BY class
    ) sub ON s.class = sub.class
WHERE 
    s.sage > sub.avg_age;
-- 写出查询,所有学生的全部科目成绩,没有参加考试的科目也要显示出来		
sql
SELECT 
    s1.Sno, s1.Sname,s1.Cno,s1.Cname,sc.score
FROM 
    (SELECT student.Sno,student.Sname,course.Cno,course.Cname
        FROM 
            student  CROSS JOIN course ) s1
LEFT JOIN 
    sc ON s1.Sno = sc.Sno AND s1.Cno = sc.Cno;
/*
5)UNION 和 UNION ALL  用于合并多个 SELECT 语句的结果集。
UNION 操作符用于合并两个或多个 SELECT 语句的结果集。它会去除合并结果集中的重复行。
要求参与 UNION 的各个 SELECT  语句的列数必须相同,并且对应列的数据类型也必须兼容(不一定完全相同,但能隐式转换)。

UNION ALL 同样用于合并多个 SELECT 语句的结果集,但它不会去除重复行。即使两个结果集中有完全相同的行,也会全部保留。
与 UNION 一样,参与 UNION ALL 的各个 SELECT 语句的列数必须相同,并且对应列的数据类型也必须兼容。
其他注意事项
1、排序:
如果要对合并后的结果集进行排序,只能在最后一个 SELECT 语句后使用 ORDER BY 子句,并且 ORDER BY 子句中的列必须是第一个 SELECT 语句中指定的列名或列的位置。
2、列别名:
可以在第一个 SELECT 语句中给列指定别名,后续 SELECT 语句中的列名会自动与第一个 SELECT 语句的列名相对应。
*/
-- 不使用分组,统计各班的人数
sql
select class 班级,count(*) 人数 from student where class="01"
UNION
select class 班级,count(*) 人数 from student where class="02"
UNION
select class 班级,count(*) 人数 from student where class="03";
# 行列转换 需要使用流程控制函数
-- 流程控制函数
-- if
sql
select if(FALSE, 'Ok', 'Error');
-- ifnull
sql
select ifnull('Ok','Default');
select ifnull('','Default');
select ifnull(null,'Default');
-- case when then else end
# 1. 等值判断
sql
CASE 表达式
    WHEN 值1 THEN 结果1
    WHEN 值2 THEN 结果2
    ...
    [ELSE 默认结果]
END
-- 逻辑:将 表达式 依次与 值1、值2 等比较,匹配成功则返回对应 结果;无匹配时返回 ELSE 结果(若省略 ELSE,返回 NULL)
# 2. 条件判断
sql
CASE
    WHEN 条件1 THEN 结果1
    WHEN 条件2 THEN 结果2
    ...
    [ELSE 默认结果]
END
-- 逻辑:依次判断 条件1、条件2 等,第一个满足的条件返回对应 结果;无匹配时返回 ELSE 结果(省略则返回 NULL)。
-- 案例1: 查询student表中的学生姓名和籍贯 (北京/上海 ----> 一线城市 , 其他 ----> 二线城市)
sql
select     sname,sbirthplace,
    ( case sbirthplace 
		when '北京' then '一线城市' 
		when '上海' then '一线城市' 
		else '二线城市' end ) as '籍贯'
from student;
-- 案例2: 统计班级各个学生的成绩,展示的规则如下:
-- >= 85,展示优秀
-- >= 60,展示及格
-- 否则,展示不及格
sql
select
    sno,score,
    (case 
		when score >= 85 then '优秀' 
		when score>=75 then '良好'
		when score >=60 then '及格' 
		else '不及格' end ) '评语'
from sc;
-- 查询各班男女生的人数和总人数
sql
SELECT class,ssex,count(*) 人数 from student GROUP BY class,ssex;
-- 这样查出来,表的数据是按行显示的,总人数也不好放入表格中

-- 每班一行显示
/*
班级  男生人数  女生人数 总人数
01          7         1       8
02          2         2       4
03          4         4       8
*/
sql
SELECT class 班级, 
sum(CASE 
	WHEN ssex="男" THEN		1
	ELSE		0
END
) 男生人数,
sum(CASE 
	WHEN ssex="女" THEN	1
	ELSE 0
END
) 女生人数,
count(*) 总人数
from student 
GROUP BY class;
-- 按班级统计各科的平均成绩
-- 有多个班级行,科目在一列上
sql
SELECT class,cname,round(avg(score),2) 平均分 
from student join sc
on student.Sno=sc.Sno
join course
on sc.Cno=course.Cno 
GROUP BY class,cname;
-- 一个班级一行,各科分列显示,平均分是具体的数据
sql
SELECT class,
round(avg(case when cname='语文' 
then score end),2) as 语文平均分,
round(avg(case when cname='数学' 
then score end),2) as 数学平均分, 
round(avg(case when cname='英语' 
then score end),2) as 英语平均分
from student left join sc
on student.Sno=sc.Sno
join course
on sc.Cno=course.Cno 
GROUP BY class;