diff --git a/Day36-40/36-38.关系型数据库MySQL.md b/Day36-40/36-38.关系型数据库MySQL.md
index b975a50..090b1bf 100644
--- a/Day36-40/36-38.关系型数据库MySQL.md
+++ b/Day36-40/36-38.关系型数据库MySQL.md
@@ -1,4 +1,4 @@
-## 关系数据库入门
+## 关系数据库MySQL
### 关系数据库概述
@@ -271,13 +271,15 @@ MySQL 在过去由于性能高、成本低、可靠性好,已经成为最流
再次使用客户端工具连接 MySQL 服务器时,就可以使用新设置的口令了。在实际开发中,为了方便用户操作,可以选择图形化的客户端工具来连接 MySQL 服务器,包括:
- - MySQL Workbench(官方工具,推荐大家使用)
+ - MySQL Workbench(官方工具)
- - Navicat for MySQL(界面简单清爽,功能直观)
+ - Navicat for MySQL(界面简单友好)
- - SQLyog for MySQL(强大的MySQL数据库管理员工具)
+
+
+ - SQLyog for MySQL(有社区版和商业版)
### MySQL 常用命令
@@ -351,6 +353,8 @@ MySQL 在过去由于性能高、成本低、可靠性好,已经成为最流
#### DDL(数据定义语言)
+下面我们来实现一个选课系统的数据库,如下所示的 SQL 创建了名为`school`的数据库和五张表,分别是学院表(`tb_college`)、学生表(`tb_student`)、教师表(`tb_teacher`)、课程表(`tb_course`)和选课记录表(`tb_record`),其中学生和教师跟学院之间是多对一关系,课程跟老师之间也是多对一关系,学生和课程是多对多关系,选课记录表就是维持学生跟课程多对多关系的中间表。
+
```SQL
-- 如果存在名为school的数据库就删除它
drop database if exists `school`;
@@ -667,6 +671,8 @@ constraint `uk_record_stu_cou` unique (`stu_id`, `cou_id`)
#### DML(数据操作语言)
+我们通过如下所示的 SQL 给上面创建的表添加数据。
+
```SQL
use school;
@@ -690,7 +696,7 @@ values
(1954, '林平之', 1, '1994-9-20', '福建莆田', 1),
(2035, '东方不败', 1, '1988-6-30', null, 2),
(3011, '林震南', 1, '1985-12-12', '福建莆田', 3),
- (3755, '项少龙', 1, '1993-1-25', null, 3),
+ (3755, '项少龙', 1, '1993-1-25', '四川成都', 3),
(3923, '杨不悔', 0, '1985-4-17', '四川成都', 3);
-- 插入老师数据
@@ -743,170 +749,320 @@ values
#### DQL(数据查询语言)
+接下来,我们完成如下所示的查询。
+
```SQL
-- 查询所有学生的所有信息
-select * from tb_student;
+select * from `tb_student`;
--- 查询学生的学号、姓名和家庭住址(投影)
-select stu_id, stu_name, stu_addr from tb_student;
+-- 查询学生的学号、姓名和籍贯(投影)
+select `stu_id`, `stu_name`, `stu_addr` from `tb_student`;
-- 查询所有课程的名称及学分(投影和别名)
-select cou_name as 课程名称, cou_credit as 学分 from tb_course;
+select `cou_name` as 课程名称, `cou_credit` as 学分 from `tb_course`;
-- 查询所有女学生的姓名和出生日期(筛选)
-select stu_name, stu_birth from tb_student where stu_sex=0;
+select `stu_name`, `stu_birth` from `tb_student` where `stu_sex`=0;
+
+-- 查询籍贯为“四川成都”的女学生的姓名和出生日期(筛选)
+select `stu_name`, `stu_birth` from `tb_student` where `stu_sex`=0 and `stu_addr`='四川成都';
+
+-- 查询籍贯为“四川成都”或者性别为“女生”的学生
+select `stu_name`, `stu_birth` from `tb_student` where `stu_sex`=0 or `stu_addr`='四川成都';
-- 查询所有80后学生的姓名、性别和出生日期(筛选)
-select stu_name, stu_sex, stu_birth from tb_student where stu_birth>='1980-1-1' and stu_birth<='1989-12-31';
-
-select stu_name, stu_sex, stu_birth from tb_student where stu_birth between '1980-1-1' and '1989-12-31';
-
--- 将表示性别的 1 和 0 处理成 “男” 和 “女”
-select stu_name as 姓名, case stu_sex when 1 then '男' else '女' end as 性别, stu_birth as 生日 from tb_student where stu_birth between '1980-1-1' and '1989-12-31';
+select `stu_name`, `stu_sex`, `stu_birth` from `tb_student`
+where `stu_birth`>='1980-1-1' and `stu_birth`<='1989-12-31';
-select stu_name as 姓名, if(stu_sex, '男', '女') as 性别, stu_birth as 生日 from tb_student where stu_birth between '1980-1-1' and '1989-12-31';
+select `stu_name`, `stu_sex`, `stu_birth` from `tb_student`
+where `stu_birth` between '1980-1-1' and '1989-12-31';
+
+-- 补充:将表示性别的 1 和 0 处理成 “男” 和 “女”
+select
+ `stu_name` as 姓名,
+ if(`stu_sex`, '男', '女') as 性别,
+ `stu_birth` as 出生日期
+from `tb_student`
+where `stu_birth` between '1980-1-1' and '1989-12-31';
+
+select
+ `stu_name` as 姓名,
+ case `stu_sex` when 1 then '男' else '女' end as 性别,
+ `stu_birth` as 出生日期
+from `tb_student`
+where `stu_birth` between '1980-1-1' and '1989-12-31';
+
+-- 查询学分大于2的课程的名称和学分(筛选)
+select `cou_name`, `cou_credit` from `tb_course` where `cou_credit`>2;
+
+-- 查询学分是奇数的课程的名称和学分(筛选)
+select `cou_name`, `cou_credit` from `tb_course` where `cou_credit`%2<>0;
+
+select `cou_name`, `cou_credit` from `tb_course` where `cou_credit` mod 2<>0;
+
+-- 查询选择选了1111的课程考试成绩在90分以上的学生学号(筛选)
+select `stu_id` from `tb_record` where `cou_id`=1111 and `score`>90;
+
+-- 查询名字叫“杨过”的学生的姓名和性别
+select `stu_name`, `stu_sex` from `tb_student` where `stu_name`='杨过';
-- 查询姓“杨”的学生姓名和性别(模糊)
-select stu_name, stu_sex from tb_student where stu_name like '杨%';
+-- % - 通配符(wildcard),它可以匹配0个或任意多个字符
+select `stu_name`, `stu_sex` from `tb_student` where `stu_name` like '杨%';
-- 查询姓“杨”名字两个字的学生姓名和性别(模糊)
-select stu_name, stu_sex from tb_student where stu_name like '杨_';
+-- _ - 通配符(wildcard),它可以精确匹配一个字符
+select `stu_name`, `stu_sex` from `tb_student` where `stu_name` like '杨_';
-- 查询姓“杨”名字三个字的学生姓名和性别(模糊)
-select stu_name, stu_sex from tb_student where stu_name like '杨__';
+select `stu_name`, `stu_sex` from `tb_student` where `stu_name` like '杨__';
-- 查询名字中有“不”字或“嫣”字的学生的姓名(模糊)
-select stu_name from tb_student where stu_name like '%不%' or stu_name like '%嫣%';
+select `stu_name` from `tb_student` where `stu_name` like '%不%' or `stu_name` like '%嫣%';
-select stu_name from tb_student where stu_name like '%嫣%'
+-- 将“岳不群”改名为“岳不嫣”,比较下面两个查询的区别
+update `tb_student` set `stu_name`='岳不嫣' where `stu_id`=1572;
+
+select `stu_name` from `tb_student` where `stu_name` like '%不%'
union
-select stu_name from tb_student where stu_name like '%不%';
+select `stu_name` from `tb_student` where `stu_name` like '%嫣%';
+
+select `stu_name` from `tb_student` where `stu_name` like '%不%'
+union all
+select `stu_name` from `tb_student` where `stu_name` like '%嫣%';
-- 查询姓“杨”或姓“林”名字三个字的学生的姓名(正则表达式模糊查询)
-select stu_name from tb_student where stu_name regexp '[杨林].{2}';
+select `stu_name` from `tb_student` where `stu_name` regexp '[杨林].{2}';
--- 查询没有录入家庭住址的学生姓名(空值)
-select stu_name from tb_student where stu_addr is null or stu_add='';
+-- 查询没有录入籍贯的学生姓名(空值处理)
+select `stu_name` from `tb_student` where `stu_addr` is null;
-select stu_name from tb_student where stu_addr<=>null or stu_addr='';
+select `stu_name` from `tb_student` where `stu_addr` <=> null;
--- 查询录入了家庭住址的学生姓名(空值)
-select stu_name from tb_student where stu_addr is not null and stu_addr<>'';
+-- 查询录入了籍贯的学生姓名(空值处理)
+select `stu_name` from `tb_student` where `stu_addr` is not null;
+
+-- 下面的查询什么也查不到,三值逻辑 --> true / false / unknown
+select `stu_name` from `tb_student` where `stu_addr`=null or `stu_addr`<>null;
-- 查询学生选课的所有日期(去重)
-select distinct sel_date from tb_record;
+select distinct `sel_date` from `tb_record`;
--- 查询学生的家庭住址(去重)
-select distinct stu_addr from tb_student where stu_addr is not null;
+-- 查询学生的籍贯(去重)
+select distinct `stu_addr` from `tb_student` where `stu_addr` is not null;
-- 查询男学生的姓名和生日按年龄从大到小排列(排序)
-select stu_name, stu_birth from tb_student where stu_sex=1 order by stu_birth asc;
+-- 升序:从小到大 - asc,降序:从大到小 - desc
+select `stu_id`, `stu_name`, `stu_birth` from `tb_student`
+where `stu_sex`=1 order by `stu_birth` asc, `stu_id` desc;
--- 将生日换算成年龄(日期函数、数值函数)
-select stu_name, stu_birth, floor(datediff(curdate(), stu_birth)/365) as stu_age from tb_student where stu_sex=1 order by stu_age desc;
+-- 补充:将上面的生日换算成年龄(日期函数、数值函数)
+select
+ `stu_id` as 学号,
+ `stu_name` as 姓名,
+ floor(datediff(curdate(), `stu_birth`)/365) as 年龄
+from `tb_student`
+where `stu_sex`=1 order by 年龄 desc, `stu_id` desc;
-- 查询年龄最大的学生的出生日期(聚合函数)
-select min(stu_birth) from tb_student;
+select min(`stu_birth`) from `tb_student`;
-- 查询年龄最小的学生的出生日期(聚合函数)
-select max(stu_birth) from tb_student;
+select max(`stu_birth`) from `tb_student`;
--- 查询编号为1111的课程考试成绩的最高分
-select max(score) from tb_record where cou_id=1111;
+-- 查询编号为1111的课程考试成绩的最高分(聚合函数)
+select max(`score`) from `tb_record` where `cou_id`=1111;
--- 查询学号为1001的学生考试成绩的最低分
-select min(score) from tb_record where stu_id=1001;
+-- 查询学号为1001的学生考试成绩的最低分(聚合函数)
+select min(`score`) from `tb_record` where `stu_id`=1001;
--- 查询学号为1001的学生考试成绩的平均分
-select avg(score) from tb_record where stu_id=1001;
-select sum(score) / count(score) from tb_record where stu_id=1001;
+-- 查询学号为1001的学生考试成绩的平均分(聚合函数)
+select avg(`score`) from `tb_record` where `stu_id`=1001;
--- 查询学号为1001的学生考试成绩的平均分,如果有null值,null值算0分
-select sum(score) / count(*) from tb_record where stu_id=1001;
-select avg(ifnull(score, 0)) from tb_record where stu_id=1001;
+select sum(`score`) / count(`score`) from `tb_record` where `stu_id`=1001;
--- 查询学号为1001的学生考试成绩的标准差
-select std(score) from tb_record where stu_id=1001;
+-- 查询学号为1001的学生考试成绩的平均分,如果有null值,null值算0分(聚合函数)
+select sum(`score`) / count(*) from `tb_record` where `stu_id`=1001;
+
+select avg(ifnull(`score`, 0)) from `tb_record` where `stu_id`=1001;
+
+-- 查询学号为1001的学生考试成绩的标准差(聚合函数)
+select std(`score`), variance(`score`) from `tb_record` where `stu_id`=1001;
-- 查询男女学生的人数(分组和聚合函数)
-select if(stu_sex, '男', '女') as 性别, count(*) as 人数 from tb_student group by stu_sex;
+select
+ case `stu_sex` when 1 then '男' else '女' end as 性别,
+ count(*) as 人数
+from `tb_student` group by `stu_sex`;
--- 查询每个学院男女学生人数
-select col_id as 学院编号, if(stu_sex, '男', '女') as 性别, count(*) as 人数 from tb_student group by col_id, stu_sex;
+-- 查询每个学院学生人数(分组和聚合函数)
+select
+ `col_id` as 学院,
+ count(*) as 人数
+from `tb_student` group by `col_id` with rollup;
+
+-- 查询每个学院男女学生人数(分组和聚合函数)
+select
+ `col_id` as 学院,
+ if(`stu_sex`, '男', '女') as 性别,
+ count(*) as 人数
+from `tb_student` group by `col_id`, `stu_sex`;
-- 查询每个学生的学号和平均成绩(分组和聚合函数)
-select stu_id as 学号, round(avg(score), 2) as 平均分 from tb_record group by stu_id;
+select
+ `stu_id`,
+ round(avg(`score`), 1) as avg_score
+from `tb_record` group by `stu_id`;
-- 查询平均成绩大于等于90分的学生的学号和平均成绩
-select stu_id as 学号, round(avg(score), 2) as 平均分 from tb_record group by stu_id having 平均分>=90;
+-- 分组以前的筛选使用where子句,分组以后的筛选使用having子句
+select
+ `stu_id`,
+ round(avg(`score`), 1) as avg_score
+from `tb_record`
+group by `stu_id` having avg_score>=90;
-- 查询1111、2222、3333三门课程平均成绩大于等于90分的学生的学号和平均成绩
-select stu_id as 学号, round(avg(score), 2) as 平均分 from tb_record where cou_id in (1111, 2222, 3333) group by stu_id having 平均分>=90;
+select
+ `stu_id`,
+ round(avg(`score`), 1) as avg_score
+from `tb_record` where `cou_id` in (1111, 2222, 3333)
+group by `stu_id` having avg_score>=90;
--- 查询年龄最大的学生的姓名(子查询)
-select stu_name from tb_student where stu_birth=(select min(stu_birth) from tb_student);
+-- 查询年龄最大的学生的姓名(子查询/嵌套查询)
+-- 嵌套查询:把一个select的结果作为另一个select的一部分来使用
+select `stu_name` from `tb_student`
+where `stu_birth`=(
+ select min(`stu_birth`) from `tb_student`
+);
-- 查询选了两门以上的课程的学生姓名(子查询/分组条件/集合运算)
-select stu_name from tb_student where stu_id in (select stu_id from tb_record group by stu_id having count(*)>2);
-
+select `stu_name` from `tb_student`
+where `stu_id` in (
+ select `stu_id` from `tb_record`
+ group by `stu_id` having count(*)>2
+);
+
-- 查询学生的姓名、生日和所在学院名称
-select stu_name, stu_birth, col_name from tb_student, tb_college where tb_student.col_id=tb_college.col_id;
+select `stu_name`, `stu_birth`, `col_name`
+from `tb_student`, `tb_college`
+where `tb_student`.`col_id`=`tb_college`.`col_id`;
-select stu_name, stu_birth, col_name from tb_student t1 inner join tb_college t2 on t1.col_id=t2.col_id;
+select `stu_name`, `stu_birth`, `col_name`
+from `tb_student` inner join `tb_college`
+on `tb_student`.`col_id`=`tb_college`.`col_id`;
-select stu_name, stu_birth, col_name from tb_student natural join tb_college;
+select `stu_name`, `stu_birth`, `col_name`
+from `tb_student` natural join `tb_college`;
-- 查询学生姓名、课程名称以及成绩(连接查询/联结查询)
-select t2.stu_id, stu_name, t3.cou_id, cou_name, score from tb_record t1, tb_student t2, tb_course t3 where t1.stu_id=t2.stu_id and t1.cou_id=t3.cou_id and score is not null;
+select `stu_name`, `cou_name`, `score`
+from `tb_student`, `tb_course`, `tb_record`
+where `tb_student`.`stu_id`=`tb_record`.`stu_id`
+and `tb_course`.`cou_id`=`tb_record`.`cou_id`
+and `score` is not null;
-select stu_name, cou_name, score from tb_student t1 inner join tb_record t2 on t1.stu_id=t2.stu_id inner join tb_course t3 on t2.cou_id=t3.cou_id where score is not null;
+select `stu_name`, `cou_name`, `score` from `tb_student`
+inner join `tb_record` on `tb_student`.`stu_id`=`tb_record`.`stu_id`
+inner join `tb_course` on `tb_course`.`cou_id`=`tb_record`.`cou_id`
+where `score` is not null;
-select stu_name, cou_name, score from tb_student natural join tb_record natural join tb_course where score is not null;
+select `stu_name`, `cou_name`, `score` from `tb_student`
+natural join `tb_record`
+natural join `tb_course`
+where `score` is not null;
--- 分页查询(前5条数据)
-select stu_name, cou_name, score from tb_student natural join tb_record natural join tb_course where score is not null order by score desc limit 5;
+-- 补充:上面的查询结果取前5条数据(分页查询)
+select `stu_name`, `cou_name`, `score`
+from `tb_student`, `tb_course`, `tb_record`
+where `tb_student`.`stu_id`=`tb_record`.`stu_id`
+and `tb_course`.`cou_id`=`tb_record`.`cou_id`
+and `score` is not null
+order by `score` desc
+limit 0,5;
--- 分页查询(6-10条数据)
-select stu_name, cou_name, score from tb_student natural join tb_record natural join tb_course where score is not null order by score desc limit 5 offset 5;
+-- 补充:上面的查询结果取第6-10条数据(分页查询)
+select `stu_name`, `cou_name`, `score`
+from `tb_student`, `tb_course`, `tb_record`
+where `tb_student`.`stu_id`=`tb_record`.`stu_id`
+and `tb_course`.`cou_id`=`tb_record`.`cou_id`
+and `score` is not null
+order by `score` desc
+limit 5 offset 5;
--- 分页查询(11-15条数据)
-select stu_name, cou_name, score from tb_student natural join tb_record natural join tb_course where score is not null order by score desc limit 5 offset 10;
-
-select stu_name, cou_name, score from tb_student natural join tb_record natural join tb_course where score is not null order by score desc limit 10,5;
+-- 补充:上面的查询结果取第11-15条数据(分页查询)
+select `stu_name`, `cou_name`, `score`
+from `tb_student`, `tb_course`, `tb_record`
+where `tb_student`.`stu_id`=`tb_record`.`stu_id`
+and `tb_course`.`cou_id`=`tb_record`.`cou_id`
+and `score` is not null
+order by `score` desc
+limit 5 offset 10;
-- 查询选课学生的姓名和平均成绩(子查询和连接查询)
-select stu_name, avg_score from tb_student t1, (select stu_id, round(avg(score),1) as avg_score from tb_record group by stu_id) t2 where t1.stu_id=t2.stu_id;
+select `stu_name`, `avg_score`
+from `tb_student` inner join (
+ select `stu_id` as `sid`, round(avg(`score`), 1) as avg_score
+ from `tb_record` group by `stu_id`
+) as `t2` on `stu_id`=`sid`;
-- 查询学生的姓名和选课的数量
-select stu_name, total from tb_student t1, (select stu_id, count(*) as total from tb_record group by stu_id) t2 where t1.stu_id=t2.stu_id;
+select `stu_name`, `total` from `tb_student` as `t1`
+inner join (
+ select `stu_id`, count(*) as `total`
+ from `tb_record` group by `stu_id`
+) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
-- 查询每个学生的姓名和选课数量(左外连接和子查询)
-select stu_name, ifnull(total, 0) as total from tb_student t1 left outer join (select stu_id, count(*) as total from tb_record group by stu_id) t2 on t1.stu_id=t2.stu_id;
+-- 左外连接:左表(写在join左边的表)的每条记录都可以查出来,不满足连表条件的地方填充null。
+select `stu_name`, coalesce(`total`, 0) as `total`
+from `tb_student` as `t1`
+left outer join (
+ select `stu_id`, count(*) as `total`
+ from `tb_record` group by `stu_id`
+) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
--- 删除tb_record表的外键约束
-alter table tb_record drop foreign key fk_record_stu_id;
-alter table tb_record drop foreign key fk_record_cou_id;
+-- 修改选课记录表,去掉 stu_id 列的外键约束
+alter table `tb_record` drop foreign key `fk_record_stu_id`;
--- 给tb_record表加两条记录,学号5566在学生表没有对应的记录
-insert into tb_record
+-- 插入两条新纪录(注意:没有学号为 5566 的学生)
+insert into `tb_record`
values
- (default, 5566, 1111, '2019-09-02', 80),
+ (default, 5566, 1111, '2019-09-02', 80),
(default, 5566, 2222, '2019-09-02', 70);
-
--- 查询学生的姓名和选课数量(右外连接)
-select t1.stu_id, stu_name, t2.stu_id, total as total from tb_student t1 right outer join (select stu_id, count(*) as total from tb_record group by stu_id) t2 on t1.stu_id=t2.stu_id;
--- 可以通过左外连接与右外连接求并集运算得到全外连接的结果
-select t1.stu_id, stu_name, t2.stu_id, total as total from tb_student t1 left outer join (select stu_id, count(*) as total from tb_record group by stu_id) t2 on t1.stu_id=t2.stu_id
+-- 右外连接:右表(写在join右边的表)的每条记录都可以查出来,不满足连表条件的地方填充null。
+select `stu_name`, `total` from `tb_student` as `t1`
+right outer join (
+ select `stu_id`, count(*) as `total`
+ from `tb_record` group by `stu_id`
+) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
+
+-- 全外连接:左表和右表的每条记录都可以查出来,不满足连表条件的地方填充null。
+-- 说明:MySQL不支持全外连接,所以用左外连接和右外连接的并集来表示。
+select `stu_name`, `total`
+from `tb_student` as `t1`
+left outer join (
+ select `stu_id`, count(*) as `total`
+ from `tb_record` group by `stu_id`
+) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`
union
-select t1.stu_id, stu_name, t2.stu_id, total as total from tb_student t1 right outer join (select stu_id, count(*) as total from tb_record group by stu_id) t2 on t1.stu_id=t2.stu_id;
+select `stu_name`, `total` from `tb_student` as `t1`
+right outer join (
+ select `stu_id`, count(*) as `total`
+ from `tb_record` group by `stu_id`
+) as `t2` on `t1`.`stu_id`=`t2`.`stu_id`;
```
上面的DML有几个地方需要加以说明:
-1. MySQL 中支持多种类型的运算符,包括:算术运算符(`+`、`-`、`*`、`/`、`%`)、比较运算符(`=`、`<>`、`<=>`、`<`、`<=`、`>`、`>=`、`BETWEEN...AND..`.、`IN`、`IS NULL`、`IS NOT NULL`、`LIKE`、`RLIKE`、`REGEXP`)、逻辑运算符(`NOT`、`AND`、`OR`、`XOR`)和位运算符(`&`、`|`、`^`、`~`、`>>`、`<<`),我们可以在 DML 中使用这些运算符处理数据。
+1. MySQL目前的版本不支持全外连接,上面我们通过`union`操作,将左外连接和右外连接的结果求并集实现全外连接的效果。大家可以通过下面的图来加深对连表操作的认识。
-2. 在查询数据时,可以在`SELECT`语句及其子句(如`WHERE`子句、`ORDER BY`子句、`HAVING`子句等)中使用函数,这些函数包括字符串函数、数值函数、时间日期函数、流程函数等,如下面的表格所示。
+
+
+2. MySQL 中支持多种类型的运算符,包括:算术运算符(`+`、`-`、`*`、`/`、`%`)、比较运算符(`=`、`<>`、`<=>`、`<`、`<=`、`>`、`>=`、`BETWEEN...AND..`.、`IN`、`IS NULL`、`IS NOT NULL`、`LIKE`、`RLIKE`、`REGEXP`)、逻辑运算符(`NOT`、`AND`、`OR`、`XOR`)和位运算符(`&`、`|`、`^`、`~`、`>>`、`<<`),我们可以在 DML 中使用这些运算符处理数据。
+
+3. 在查询数据时,可以在`SELECT`语句及其子句(如`WHERE`子句、`ORDER BY`子句、`HAVING`子句等)中使用函数,这些函数包括字符串函数、数值函数、时间日期函数、流程函数等,如下面的表格所示。
常用字符串函数。
@@ -977,21 +1133,17 @@ select t1.stu_id, stu_name, t2.stu_id, total as total from tb_student t1 right o
#### DCL(数据控制语言)
+数据控制语言用于给指定的用户授权或者从召回指定用户的指定权限,这组操作对数据库管理员来说比较重要,将一个用户的权限最小化(刚好够用)是非常重要的,对数据库的安全至关重要。
+
```SQL
--- 创建可以远程登录的root账号并为其指定口令
-create user 'root'@'%' identified by '123456';
+-- 创建名为 wangdachui 的账号并为其指定口令,允许该账号从任意主机访问
+create user 'wangdachui'@'%' identified by '123456';
--- 为远程登录的root账号授权操作所有数据库所有对象的所有权限并允许其将权限再次赋予其他用户
-grant all privileges on *.* to 'root'@'%' with grant option;
+-- 授权 wangdachui 可以对名为school的数据库执行 select 和 insert 操作
+grant select, insert on `school`.* to 'wangdachui'@'%';
--- 创建名为hellokitty的用户并为其指定口令
-create user 'hellokitty'@'%' identified by '123123';
-
--- 将对school数据库所有对象的所有操作权限授予hellokitty
-grant all privileges on school.* to 'hellokitty'@'%';
-
--- 召回hellokitty对school数据库所有对象的insert/delete/update权限
-revoke insert, delete, update on school.* from 'hellokitty'@'%';
+-- 召回 wangdachui 对school数据库的 insert 权限
+revoke insert on `school`.* from 'wangdachui'@'%';
```
> **说明**:创建一个可以允许任意主机登录并且具有超级管理员权限的用户在现实中并不是一个明智的决定,因为一旦该账号的口令泄露或者被破解,数据库将会面临灾难级的风险。
@@ -1028,15 +1180,15 @@ possible_keys: NULL
在上面的 SQL 执行计划中,有几项值得我们关注:
1. `select_type`:查询的类型。
- - SIMPLE:简单 SELECT,不需要使用 UNION 操作或子查询。
- - PRIMARY:如果查询包含子查询,最外层的 SELECT 被标记为 PRIMARY。
- - UNION:UNION 操作中第二个或后面的 SELECT 语句。
- - SUBQUERY:子查询中的第一个 SELECT。
- - DERIVED:派生表的 SELECT 子查询。
+ - `SIMPLE`:简单 SELECT,不需要使用 UNION 操作或子查询。
+ - `PRIMARY`:如果查询包含子查询,最外层的 SELECT 被标记为 PRIMARY。
+ - `UNION`:UNION 操作中第二个或后面的 SELECT 语句。
+ - `SUBQUERY`:子查询中的第一个 SELECT。
+ - `DERIVED`:派生表的 SELECT 子查询。
2. `table`:查询对应的表。
-3. `type`:MySQL 在表中找到满足条件的行的方式,也称为访问类型,包括:ALL(全表扫描)、index(索引全扫描,只遍历索引树)、range(索引范围扫描)、ref(非唯一索引扫描)、eq_ref(唯一索引扫描)、const / system(常量级查询)、NULL(不需要访问表或索引)。在所有的访问类型中,很显然 ALL 是性能最差的,它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。
+3. `type`:MySQL 在表中找到满足条件的行的方式,也称为访问类型,包括:`ALL`(全表扫描)、`index`(索引全扫描,只遍历索引树)、`range`(索引范围扫描)、`ref`(非唯一索引扫描)、`eq_ref`(唯一索引扫描)、`const` / `system`(常量级查询)、`NULL`(不需要访问表或索引)。在所有的访问类型中,很显然 ALL 是性能最差的,它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。
4. `possible_keys`:MySQL 可以选择的索引,但是**有可能不会使用**。
-5. `key`:MySQL 真正使用的索引,如果为NULL就表示没有使用索引。
+5. `key`:MySQL 真正使用的索引,如果为`NULL`就表示没有使用索引。
6. `key_len`:使用的索引的长度,在不影响查询的情况下肯定是长度越短越好。
7. `rows`:执行查询需要扫描的行数,这是一个**预估值**。
8. `extra`:关于查询额外的信息。
@@ -1125,7 +1277,7 @@ drop index idx_student_name on tb_student;
2. 索引列的基数越大(取值多、重复值少),索引的效果就越好。
3. 使用**前缀索引**可以减少索引占用的空间,内存中可以缓存更多的索引。
4. **索引不是越多越好**,虽然索引加速了读操作(查询),但是写操作(增、删、改)都会变得更慢,因为数据的变化会导致索引的更新,就如同书籍章节的增删需要更新目录一样。
-5. 使用 InnoDB 存储引擎时,表的普通索引都会保存主键的值,所以**主键要尽可能选择较短的数据类型**,这样可以有效的减少索引占用的空间,利用提升索引的缓存效果。
+5. 使用 InnoDB 存储引擎时,表的普通索引都会保存主键的值,所以**主键要尽可能选择较短的数据类型**,这样可以有效的减少索引占用的空间,提升索引的缓存效果。
最后,还有一点需要说明,InnoDB 使用的 B-tree 索引,数值类型的列除了等值判断时索引会生效之外,使用`>`、`<`、`>=`、`<=`、`BETWEEN...AND... `、`<>`时,索引仍然生效;对于字符串类型的列,如果使用不以通配符开头的模糊查询,索引也是起作用的,但是其他的情况会导致索引失效,这就意味着很有可能会做全表查询。
@@ -1139,21 +1291,22 @@ drop index idx_student_name on tb_student;
1. 可以将实体数据表隐藏起来,让外部程序无法得知实际的数据结构,让访问者可以使用表的组成部分而不是整个表,降低数据库被攻击的风险。
2. 在大多数的情况下视图是只读的(更新视图的操作通常都有诸多的限制),外部程序无法直接透过视图修改数据。
3. 重用 SQL 语句,将高度复杂的查询包装在视图表中,直接访问该视图即可取出需要的数据;也可以将视图视为数据表进行连接查询。
-4. 视图可以返回与实体数据表不同格式的数据,
+4. 视图可以返回与实体数据表不同格式的数据,在创建视图的时候可以对数据进行格式化处理。
创建视图。
```SQL
-create view vw_avg_score
+-- 创建视图
+create view `vw_avg_score`
as
- select sid, round(avg(score), 1) as avgscore
- from tb_record group by sid;
+ select `stu_id`, round(avg(`score`), 1) as `avg_score`
+ from `tb_record` group by `stu_id`;
-create view vw_student_score
+-- 基于已有的视图创建视图
+create view `vw_student_score`
as
- select stuname, avgscore
- from tb_student, vw_avg_score
- where stuid=sid;
+ select `stu_name`, `avg_score`
+ from `tb_student` natural join `vw_avg_score`;
```
> **提示**:因为视图不包含数据,所以每次使用视图时,都必须执行查询以获得数据,如果你使用了连接查询、嵌套查询创建了较为复杂的视图,你可能会发现查询性能下降得很厉害。因此,在使用复杂的视图前,应该进行测试以确保其性能能够满足应用的需求。
@@ -1161,7 +1314,7 @@ as
使用视图。
```SQL
-select stuname, avgscore from vw_student_score order by avgscore desc;
+select * from `vw_student_score` order by `avg_score` desc;
```
```
@@ -1213,29 +1366,23 @@ drop view vw_student_score;
下面的过程实现了查询某门课程的最高分、最低分和平均分。
```SQL
-drop procedure if exists sp_score_by_cid;
+drop procedure if exists sp_score_stat;
delimiter $$
-create procedure sp_score_by_cid(
+create procedure sp_score_stat(
courseId int,
out maxScore decimal(4,1),
out minScore decimal(4,1),
out avgScore decimal(4,1)
)
begin
- select max(score) into maxScore from tb_record
- where cid=courseId;
- select min(score) into minScore from tb_record
- where cid=courseId;
- select avg(score) into avgScore from tb_record
- where cid=courseId;
+ select max(score) into maxScore from tb_record where cou_id=courseId;
+ select min(score) into minScore from tb_record where cou_id=courseId;
+ select avg(score) into avgScore from tb_record where cou_id=courseId;
end $$
delimiter ;
-
-call sp_score_by_cid(1111, @a, @b, @c);
-select @a, @b, @c;
```
> **说明**:在定义过程时,因为可能需要书写多条 SQL,而分隔这些 SQL 需要使用分号作为分隔符,如果这个时候,仍然用分号表示整段代码结束,那么定义过程的 SQL 就会出现错误,所以上面我们用`delimiter $$`将整段代码结束的标记定义为`$$`,那么代码中的分号将不再表示整段代码的结束,整段代码只会在遇到`end $$`时才会执行。在定义完过程后,通过`delimiter ;`将结束符重新改回成分号(恢复现场)。
@@ -1245,7 +1392,7 @@ select @a, @b, @c;
调用过程。
```SQL
-call sp_score_by_cid(1111, @a, @b, @c);
+call sp_score_stat(1111, @a, @b, @c);
```
获取输出参数的值。
@@ -1257,7 +1404,7 @@ select @a as 最高分, @b as 最低分, @c as 平均分;
删除过程。
```SQL
-drop procedure sp_score_by_cid;
+drop procedure sp_score_stat;
```
在过程中,我们可以定义变量、条件,可以使用分支和循环语句,可以通过游标操作查询结果,还可以使用事件调度器,这些内容我们暂时不在此处进行介绍。虽然我们说了很多过程的好处,但是在实际开发中,如果过度的使用过程并将大量复杂的运算放到过程中,必然会导致占用数据库服务器的 CPU 资源,造成数据库服务器承受巨大的压力。为此,我们一般会将复杂的运算和处理交给应用服务器,因为很容易部署多台应用服务器来分摊这些压力。
@@ -1274,10 +1421,109 @@ MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源
上面语法中,窗口函数的位置可以放以下两种函数:
-1. 专用窗口函数,包括:`lead`、`rank`、`dense_rank`和`row_number`等。
+1. 专用窗口函数,包括:`lead`、`lag`、`first_value`、`last_value`、`rank`、`dense_rank`和`row_number`等。
2. 聚合函数,包括:`sum`、`avg`、`max`、`min`和`count`等。
-> **参考链接**:。
+下面为大家举几个使用窗口函数的简单例子,我们先用如下所示的 SQL 建库建表。
+
+```SQL
+-- 创建名为hrs的数据库并指定默认的字符集
+create database `hrs` default charset utf8mb4;
+
+-- 切换到hrs数据库
+use `hrs`;
+
+-- 创建部门表
+create table `tb_dept`
+(
+`dno` int not null comment '编号',
+`dname` varchar(10) not null comment '名称',
+`dloc` varchar(20) not null comment '所在地',
+primary key (`dno`)
+);
+
+-- 插入4个部门
+insert into `tb_dept` values
+ (10, '会计部', '北京'),
+ (20, '研发部', '成都'),
+ (30, '销售部', '重庆'),
+ (40, '运维部', '深圳');
+
+-- 创建员工表
+create table `tb_emp`
+(
+`eno` int not null comment '员工编号',
+`ename` varchar(20) not null comment '员工姓名',
+`job` varchar(20) not null comment '员工职位',
+`mgr` int comment '主管编号',
+`sal` int not null comment '员工月薪',
+`comm` int comment '每月补贴',
+`dno` int not null comment '所在部门编号',
+primary key (`eno`),
+constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
+constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
+);
+
+-- 插入14个员工
+insert into `tb_emp` values
+ (7800, '张三丰', '总裁', null, 9000, 1200, 20),
+ (2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
+ (3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
+ (3211, '张无忌', '程序员', 2056, 3200, null, 20),
+ (3233, '丘处机', '程序员', 2056, 3400, null, 20),
+ (3251, '张翠山', '程序员', 2056, 4000, null, 20),
+ (5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
+ (5234, '郭靖', '出纳', 5566, 2000, null, 10),
+ (3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
+ (1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
+ (4466, '苗人凤', '销售员', 3344, 2500, null, 30),
+ (3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
+ (3577, '杨过', '会计', 5566, 2200, null, 10),
+ (3588, '朱九真', '会计', 5566, 2500, null, 10);
+```
+
+例子1:查询按月薪从高到低排在第4到第6名的员工的姓名和月薪。
+
+```SQL
+select * from (
+ select
+ `ename`, `sal`,
+ row_number() over (order by `sal` desc) as `rank`
+ from `tb_emp`
+) `temp` where `rank` between 4 and 6;
+```
+
+> **说明**:上面使用的函数`row_number()`可以为每条记录生成一个行号,在实际工作中可以根据需要将其替换为`rank()`或`dense_rank()`函数,三者的区别可以参考官方文档或阅读[《通俗易懂的学会:SQL窗口函数》](https://zhuanlan.zhihu.com/p/92654574)进行了解。在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。
+>
+> ```SQL
+> select `rank`, `ename`, `sal` from (
+> select @a:=@a+1 as `rank`, `ename`, `sal`
+> from `tb_emp`, (select @a:=0) as t1 order by `sal` desc
+> ) t2 where `rank` between 4 and 6;
+> ```
+
+例子2:查询每个部门月薪最高的两名的员工的姓名和部门名称。
+
+```SQL
+select `ename`, `sal`, `dname`
+from (
+ select
+ `ename`, `sal`, `dno`,
+ rank() over (partition by `dno` order by `sal` desc) as `rank`
+ from `tb_emp`
+) as `temp` natural join `tb_dept` where `rank`<=2;
+```
+
+> 说明:在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。
+>
+> ```SQL
+> select `ename`, `sal`, `dname` from `tb_emp` as `t1`
+natural join `tb_dept`
+where (
+ select count(*) from `tb_emp` as `t2`
+ where `t1`.`dno`=`t2`.`dno` and `t2`.`sal`>`t1`.`sal`
+)<2 order by `dno` asc, `sal` desc;
+> ```
### 其他内容
@@ -1289,6 +1535,8 @@ MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源
2. 第二范式:数据表里的所有数据都要和该数据表的键(主键与候选键)有完全依赖关系。
3. 第三范式:所有非键属性都只和候选键有相关性,也就是说非键属性之间应该是独立无关的。
+> **说明**:实际工作中,出于效率的考虑,我们在设计表时很有可能做出反范式设计,即故意降低方式级别,增加冗余数据来获得更好的操作性能。
+
#### 数据完整性
1. 实体完整性 - 每个实体都是独一无二的
@@ -1297,7 +1545,7 @@ MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源
2. 引用完整性(参照完整性)- 关系中不允许引用不存在的实体
- 外键(foreign key)
-3. 域完整性 - 数据是有效的
+3. 域(domain)完整性 - 数据是有效的
- 数据类型及长度
- 非空约束(not null)
@@ -1326,12 +1574,6 @@ MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源
start transaction
```
- 或
-
- ```SQL
- begin
- ```
-
- 提交事务
```SQL
@@ -1350,63 +1592,7 @@ MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源
### Python数据库编程
-我们用如下所示的 SQL 创建数据库,然后为大家演示在 Python 中如何访问 MySQL 数据库。
-
-```SQL
-drop database if exists hrs;
-create database hrs default charset utf8;
-
-use hrs;
-
-drop table if exists tb_emp;
-drop table if exists tb_dept;
-
-create table tb_dept
-(
-dno int not null comment '编号',
-dname varchar(10) not null comment '名称',
-dloc varchar(20) not null comment '所在地',
-primary key (dno)
-);
-
-insert into tb_dept values
- (10, '会计部', '北京'),
- (20, '研发部', '成都'),
- (30, '销售部', '重庆'),
- (40, '运维部', '深圳');
-
-create table tb_emp
-(
-eno int not null comment '员工编号',
-ename varchar(20) not null comment '员工姓名',
-job varchar(20) not null comment '员工职位',
-mgr int comment '主管编号',
-sal int not null comment '员工月薪',
-comm int comment '每月补贴',
-dno int comment '所在部门编号',
-primary key (eno)
-);
-
-alter table tb_emp add constraint fk_emp_dno foreign key (dno) references tb_dept (dno);
-
-insert into tb_emp values
- (7800, '张三丰', '总裁', null, 9000, 1200, 20),
- (2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
- (3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
- (3211, '张无忌', '程序员', 2056, 3200, null, 20),
- (3233, '丘处机', '程序员', 2056, 3400, null, 20),
- (3251, '张翠山', '程序员', 2056, 4000, null, 20),
- (5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
- (5234, '郭靖', '出纳', 5566, 2000, null, 10),
- (3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
- (1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
- (4466, '苗人凤', '销售员', 3344, 2500, null, 30),
- (3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
- (3577, '杨过', '会计', 5566, 2200, null, 10),
- (3588, '朱九真', '会计', 5566, 2500, null, 10);
-```
-
-在 Python3 中,我们通常使用纯 Python 的三方库 PyMySQL 来访问 MySQL 数据库,它应该是目前 Python 操作 MySQL 数据库最好的选择。
+在 Python3 中,我们可以使用`mysqlclient`或`pymysql`三方库来实现对 MySQL 数据库的操作,二者的用法完全相同,只是导入的模块名不一样。我们推荐大家使用纯 Python 的三方库`pymysql`,因为它更容易安装成功。下面我们以之前创建的名为`hrs`的数据库为例,为大家演示如何通过 Python 程序操作 MySQL 数据库实现数据持久化操作。
1. 安装 PyMySQL。