目录
- 前言
- 1. 数据库约束
- 1.1. 约束类型
- not null 约束
- unique 唯一约束
- default 默认值约束
- primary key 主键约束
- foreign key 外键约束
- 2. 表的设计
- 2.1. 实体之间的关系
- 一对一
- 一对多
- 多对多
- 3. 新增
- 4. 查询
- 4.1. 聚合查询
- 4.1.1. 聚合函数
- 4.1.2. group by 子句
- 4.1.3. having
- 4.2. 联合查询 (多表查询)
- 4.2.1. 内连接
- 4.2.2. 外连接 (比较少用)
- 4.2.3. 自连接 (比较少用)
- 4.2.4. 子查询 (嵌套查询, 不推荐使用)
- 4.2.5. 合并查询
前言
进阶内容主要是针对查询
新增,修改,删除基本没有进阶的内容了
1. 数据库约束
约束就是创建表的时候,给这个表指定的一些规则,后续插入/修改/删除时都要保证数据是能够遵守这些规则的
引入规则,是为了进行更强的数据检查/校验
1.1. 约束类型
如果后续插入/修改的数据不符合约束类型,就会报错
not null 约束
指示某列不能存储 NULL 值 (从选填项变成必填项)
此时创建了一个没有任何约束的表 student, 可以往里面插入空值,此时我们删除 student 表,重新创建一个有约束的 student 表
此时是对列 id 进行了 not null 约束,对 student 表的描述中, id 中不能为 NULL, 但 name 可以为 NULL
unique 唯一约束
保证某列的每行的值必须是唯一的, unique 可以指定多个列
如果我再插入 id 为 1 , name 为 张三 还是可以的,因为 此时 id 和 name 都没有 unique 约束
有时候我们会期望数据是唯一的,不要重复出现,就可以加上 unique 约束
UNI 就是 unique 的缩写,表示 id 这一列的每个数据都是唯一的
duplicate 重复的
加上 unique 约束之后,后续进行插入/修改的时候,都会先进行查询,看看当前这个值是否已经存在
default 默认值约束
规定没有给列赋值时的默认值
默认情况下的默认值就是 NULL
此时没有给列赋值时的默认值就是 NULL,我们可以用 default 调整默认值,这里我们删除这个表,重新创建一个有 default 约束的表
然后再进行指定列插入
很多时候,如果返回一个 null,是一个不太好的体验,尤其是不应该把 null 这样的字眼呈现到普通用户的眼前
primary key 主键约束
primary key 相当于 not null 约束 和 unique 约束 的结合
主键 是数据库中最重要的约束了
用来作为一个记录的身份标识 (就像身份证号)
注意: 一个表当中只能有一个 primary key
主键是不允许重复的,我们怎么保证我们输入的数据是不重复的呢? mysql 自身给我们提供了一种机制 - 自增主键
当前这个 id 就不需要用户自己指定了,可以交给数据库,让数据库自行进行分配,数据库会按照自增的方式来分配 1,2,3,4…(前提是自增的数据得是整型才能自增)
这里的 id 为什么可以插入 null,被主键约束不是不能为空吗? 插入 null 为了可以让数据库自行分配
这里的 id 手动指定也是可以的
当手动指定了之后,还是要确保,当前你插入的结果是不重复的
每次使用 null 的方式插入自增主键的时候,数据库会根据当前这一列的最大值 的基础上继续进行递增.而且这里的递增是不会重复利用之前的值的,除非手动插入一个 id 为 5 的数据 …
上述自增主键,只能在单个数据库下生效
如果数据库是由多个 mysql 服务器构成的集群,此时,自增主键就无法生效了
foreign key 外键约束
描述的是两个表之间的关联关系,为了方便举例,我们创建两个有关联的表 class 和 student
创建3个班级
创建3个学生
像王五这个同学,不在班级表中存在,因此王五同学就是一个不太科学的非法数据
引入外键约束,就是为了解决这样的问题
希望学生表中的 classId 都要在班级表中存在
就可以使用外键约束进行校验
外键约束与其他约束的写法不同;
其他约束都是那一列需要约束,就创建到那一列的后面,
外键约束,则是写到最后的,把所有前面的列都定义好了之后,在最后面通过 foreign key 创建外键约束
格式是: foreign key (指定本表哪个列) references 其他表名(其他表名中的列)
此时就要求: 本表中这个列的数据必须要在引用的外面的表的对应列中存在
这个例子的意思就是每一个同学的 classId 得在班级表中的 classId 里存在
这个情况下,也可以认为: 班级表约束了学生表
把班级表这种约束别人的表,称为 ‘父表’ (parent table)
把学生表这种被别人约束的表,称为 ‘子表’ (child table)
这个被引用的列 classId ,得带有索引,才能被子表引用,因为 class 表里有了主键约束之后,就会自动创建出索引,能够加快查询速度,要求父表要有主键约束才能引用
constraint 约束
由于此时班级表是空着的,1 这个 classId 一定不在班级表中存在
引入外键约束之后,新增一个记录,就会先在对应的父表中查询,看看是否存在,如果不存在,就会报错
添加或修改不满足约束的数据都会报错
表面上是父表约束子表,实际上是双向约束,如果尝试删除父表中的数据,就可能有问题
想要删除父表中的数据,就必须先删除子表中的对应的数据,确保子表中没有数据引用父表里的数据,才能真正执行删除
2. 表的设计
数据库设计就是根据需求,来把你需要的表给创建出来 (有几个表, 每个表里都有是什么…)
设计原则:
- 先根据需求,找到实体 (像是面向对象,一般每个实体都得安排一个表)
- 梳理清楚实体之间的关系
2.1. 实体之间的关系
实体之间的关系有4种:
- 一对一
- 一对多
- 多对多
- 没关系(两个实体毫不相干)
一对一
模版:
一个学生,可以有一个学生账户
一个学生账户,也只能给一个学生使用
一对多
模版:
一个班级,可以包含多个学生
一个学生,只能从属于一个班级
多对多
模版:
一个学生,可以选择多门课程
一个课程,也可以被多个学生选择
多对多可以再搞个第三个表将两个表联系起来,可以实现多对多的关系
3. 新增
其实是把插入语句和查询语句结合到一起
这里是以查询的结果,来作为插入的值,比如下面的 insert into 表名 select …, 这里的select 代替了 values… ,表示将查询到的结果直接插入到这个表中
这里需要注意查询出来的结果集合,列数和类型需要互相匹配,列名不相同没关系
4. 查询
4.1. 聚合查询
4.1.1. 聚合函数
在之前的基础查询当中有说过表达式查询的例子: select name, chinese + math + english from …
这里的 chinese + math + english 是列与列之间进行运算.
而聚合查询是行与行之间进行运算,此处行之间的运算有一定的限制,不像表达式查询写的那么自由
聚合查询只能通过 SQL 提供的一些聚合函数 (库函数) 来进行操作
下面列举一些最常用的聚合函数:
- count(表达式)
- sum(表达式)
- avg(表达式)
- max(表达式)
- min(表达式)
sum,avg,max,min中的表达式得是数字才有意义
接着还是举个例子来展示聚合函数的使用效果:
举例用的数据源码:
-- 创建考试成绩表
DROP TABLE IF EXISTS exam_result;
CREATE TABLE exam_result (
id INT,
name VARCHAR(20),
chinese DECIMAL(3,1),
math DECIMAL(3,1),
english DECIMAL(3,1)
);
-- 插入测试数据
INSERT INTO exam_result (id,name, chinese, math, english) VALUES
(1,'唐三藏', 67, 98, 56),
(2,'孙悟空', 87.5, 78, 77),
(3,'猪悟能', 88, 98.5, 90),
(4,'曹孟德', 82, 84, 67),
(5,'刘玄德', 55.5, 85, 45),
(6,'孙权', 70, 73, 78.5),
(7,'宋公明', 75, 65, 30);
count() 聚合函数操作就相当于:
- 执行 select * from exam_result
- 再使用 count 来计算结果的行数
注意: 如果查询结果带有 null 值,此时 null 值的记录是不会计入计数的,但在使用 count(*) 的时候,如果有全是 null 的行,也会记录起来
sum会把这一列的值全部在一起进行相加,但是这个过程中,会把 null 给忽略掉
avg, max, min 的用法和 sum 基本类似的
如果我想查询整个表里,语文成绩最低的同学…
大部分情况,聚合的列和非聚合的列,不能在一起配合使用,有一种情况除外 (group by)
一种比较简单直观的写法:
4.1.2. group by 子句
刚才的聚合,是把整个表的所有行,都聚合在一处了
但是有时候,也可以把所有的行,分成若干组,每个组都能分别进行聚合
格式: … group by 列名
效果就是把这个指定的列,把值相同的记录都划分到一组,针对这些组就可以分别进行聚合查询了
分组操作,往往就是和 聚合 配合使用的
这里创建了一个员工表当做例子, 其中 role 列里有些人的岗位都是一样的,有时候就需要通过岗位进行一些查询或者统计相关的工作,这就可以使用 group by 了
比如查询每个岗位的平均薪资是多少
这个 role 是 没有使用聚合函数的列,本来是不能够和 聚合函数 一起使用的,但是 role 被 group by 了
按照 role 来 group by ,所以写 select 列名的时候,可以写 role ,每个组里的 role 都是相同的 ,但是不能写 id / name
4.1.3. having
针对上面的分组查询,也是可以使用条件的
针对的查询结果进行条件筛选:
- 分组之前的条件,使用 where 来表示
- 分组之后的条件,使用 having 来表示
having 和 where 的用法是一样的
比如:
-
统计除了张三之外的人, 每个岗位的平均薪资 , 这里是在分组之前,先把张三除去了,这个条件不必分组就能得到,这个时候就用 where
-
统计每个岗位的平均薪资,刨除平均薪资 >= 20000 的情况, 因为平均薪资必须要分组之后, 才能计算, 这个时候就用 having
-
查询每个岗位的平均薪资,除去 张三,也除去 平均薪资 >= 20000 的情况, 这个时候 where 和 having 就得同时使用了
4.2. 联合查询 (多表查询)
前面的查询,都是针对一张表的
多表查询,就要比单表查询更复杂一些
多表查询是对多张表的数据取笛卡尔积
笛卡尔积的列数,就是之前两个表的列数之和
笛卡尔积的行数,就是之前两个表的行数之积
如果表更多,就可以两两进行笛卡尔积
多表联合查询的核心操作就是进行笛卡尔积
比如使用两个表进行联合查询,就是先把这两个表计算笛卡尔积,然后再指定一些条件,来实现需求中的一些查询结果
实际开发中,笛卡尔积(多表查询) 一定要慎重使用,使用之前一定要评估好笛卡尔积的规模,如果基于两个很大的表进行笛卡尔积,可能就会产生大量的运算和 IO,可能就会把数据库给搞挂了…
下面通过创建两个表来演示:
SQL 中只需使用 select …from 多个表名 就能进行笛卡尔积
在笛卡尔积的基础上, 使用 where 条件表示出来, 得到的结果,就全都是有意义的数据了
像 student.classId = class.classId 这种条件就叫做连接条件,笛卡尔积就相当于把多个表给连接到一起了
在进行多表查询的时候,往往就是:
- 计算笛卡尔积
- 指定连接条件
- 根据需求指定其他条件
- 针对列进行精简/使用聚合函数
为了方便举例多表查询, 这里重新构造4张表:
举例用的题目数据源码:
drop table if exists classes;
drop table if exists student;
drop table if exists course;
drop table if exists score;
create table classes (id int primary key auto_increment, name varchar(20), `desc` varchar(100));
create table student (id int primary key auto_increment, sn varchar(20), name varchar(20), qq_mail varchar(20), classes_id int);
create table course(id int primary key auto_increment, name varchar(20));
create table score(score decimal(3, 1), student_id int, course_id int);
insert into classes(name, `desc`) values
('计算机系2019级1班', '学习了计算机原理、C和Java语言、数据结构和算法'),
('中文系2019级3班','学习了中国传统文学'),
('自动化2019级5班','学习了机械自动化');
insert into student(sn, name, qq_mail, classes_id) values
('09982','黑旋风李逵','xuanfeng@qq.com',1),
('00835','菩提老祖',null,1),
('00391','白素贞',null,1),
('00031','许仙','xuxian@qq.com',1),
('00054','不想毕业',null,1),
('51234','好好说话','say@qq.com',2),
('83223','tellme',null,2),
('09527','老外学中文','foreigner@qq.com',2);
insert into course(name) values
('Java'),('中国传统文化'),('计算机原理'),('语文'),('高阶数学'),('英文');
insert into score(score, student_id, course_id) values
-- 黑旋风李逵
(70.5, 1, 1),(98.5, 1, 3),(33, 1, 5),(98, 1, 6),
-- 菩提老祖
(60, 2, 1),(59.5, 2, 5),
-- 白素贞
(33, 3, 1),(68, 3, 3),(99, 3, 5),
-- 许仙
(67, 4, 1),(23, 4, 3),(56, 4, 5),(72, 4, 6),
-- 不想毕业
(81, 5, 1),(37, 5, 5),
-- 好好说话
(56, 6, 2),(43, 6, 4),(79, 6, 6),
-- tellme
(80, 7, 2),(92, 7, 6);
下面在创建 classes 表的时候, desc需要加上反引号,因为在 SQL 中, desc 是降序的关键字
这四个表分别是学生, 班级, 课程, 分数
班级和学生是一对多的关系, 学生和课程是多对多的关系
分数表就是课程和同学之间的关联表
4.2.1. 内连接
内连接和外连接的区别在本文章的 4.2.2. 外连接中有说明
以下3个例题都是内连接
例题1: 查询’许仙’同学的成绩
分析: 学生名字在学生表中, 成绩在成绩表中,需要将学生表和成绩表进行联合查询
第一种写法: select * from 表1,表2 where 条件…;
-
笛卡尔积: select * from student, score;
-
指定连接条件: select * from student, score where student.id = score.student_id;
-
根据需求指定其他条件: select * from student, score where student.id = score.student_id and student.name = ‘许仙’;
-
针对列进行精简 / 表达式 / 使用聚合函数: select student.name, score.score from student, score where student.id = score.student_id and student.name = ‘许仙’;
第二种写法: select * from 表1 join 表2 on 条件…;
- 计算笛卡尔积: select * from student join score;
- 指定连接条件: select * from student join score on student.id = score.student_id;
- 根据需求指定其他条件: select * from student join score on student.id = score.student_id and student.name = ‘许仙’;
- 针对列进行精简 / 表达式 / 使用聚合函数: select student.name, score.score from student join score on student.id = score.student_id and student.name = ‘许仙’;
例题2: 查询每个同学的总成绩,以及同学的个人信息
分析:
-
此时成绩是按照 行 的方式排列,要想加和,就需要使用聚合查询了
-
需要每个同学的总成绩,就要按照 同学 的 id 进行 group by (名字可能重名)
解题:
-
计算笛卡尔积: select * from student, score;
-
指定连接条件: select * from student, score where student.id = score.student_id;
-
根据需求指定其他条件: 由于此处是需要所有同学的总成绩,不需要对同学进行进一步的筛选,所以没有此步骤
-
针对列进行精简 / 表达式 / 使用聚合函数: select student.id, student.name, sum(score.score) as total from student, score where student.id = score.student_id group by student.id;
例题3: 查询所有同学的成绩,以及同学的个人信息
分析: 需要 student表, course表, score表 这三张表的连接,要两个连接条件, 观察这三个表发现: score表可以将 student表 和 course 表联系起来
解题:
-
计算笛卡尔积: select * from student, course, score;
-
指定连接条件: select * from student, course, score where student.id = score.student_id and course.id = score.course_id;
-
根据需求指定其他条件: 我们已经通过第二步就可以得到所有的有效信息,这一步省略
-
针对列进行精简 / 表达式 / 使用聚合函数: select student.name as studentName, course.name as courseName, score.score from student, course, score where course.id = score.course_id and student.id = score.student_id;
4.2.2. 外连接 (比较少用)
外连接的实际应用很少
外连接也是 多表查询 的一种体现形式
多数情况下,内连接和外连接,查询结果没啥区别,只有一些特殊情况下,查询结果才会存在差异
这里再创建两个表举例:
student 表 和 score 表 的数据是一一对应的
学生表中的任何一个记录都能在分数表中体现出来
反之,分数表中的每个记录,也能在学生表中体现出来
此时,内连接和外连接的结果都是一样的
内连接的两种写法:
join 前面还可以加上 inner ,inner join 就表示内连接, inner 可以省略
外连接 , 分成 左外连接 (left) 和 右外连接 (right)
在当前数据的情况下,内连接和外连接的结果是一样的
如果对这两个表的数据进行修改,让这两个表的记录不再一一对应,此时内连接和外连接的结果就不同了
此时学生表中的王五没有在成绩表中对应,成绩表中的 4 号学生 id 也没有在学生表中对应 (这种情况属实是不太常见的情况)
内连接的结果: 对于内连接来说,得到的结果,必须是同时在两个表中都存在的数据
外连接的结果:
- 左外连接, 就是以左表为主, 保证左侧的表中的每个记录都体现在最终结果里,如果这个记录在右表中没有匹配,就把对应的列填成 NULL
- 右外连接以右表为主,会保证右表中的每个记录都存在,如果对应的数据在左表中没有,就会填成 NULL
4.2.3. 自连接 (比较少用)
自连接: 同一张表自己和自己进行笛卡尔积
有的时候, 要想进行条件查询, 条件一定是列和列之间, 不能是行和行之间指定条件.
有时候就想针对 行 之间指定条件, 此时就可以通过自连接,把 行 关系 转成 列 关系…
例题: 显示所有 计算机原理的成绩 比 Java成绩 高的成绩信息
分析: 在这个例子中,要想完成不同科目的比较,就需要比较行之间的大小 (在 sql 中无法直接做到), 就只能把行转成列
解题: 先自己和自己进行笛卡尔积
查询的两个表名不能相同,解决办法: 给这个表起不同的别名 (alias)
然后指定条件: 此处要关注的是每个学生自己的两门课的成绩情况,所以就按照学生 id 作为连接条件
然后需要筛选出所有左表为 3 ,右表为 1 的记录 (等价于筛选左表为 1,右表为 3 的记录)
有修java和计算机原理的所有学生都筛选完毕,接下来就是分数的比较
如果要显示名字还要和学生表进行笛卡尔积
最后可以再对列进行精简
4.2.4. 子查询 (嵌套查询, 不推荐使用)
把多个简单的 sql, 合并成一个 复杂 sql
例题: 查询与 ‘‘不想毕业’’ 同学的同班同学
分析: 先找到 ‘‘不想毕业’’ 的班级 id, 再拿着这个 班级id 来进行查询其他同学
以上是正常的做法
因为 classes_id = 1 中的1是从 select classes_id from student where name = ‘不想毕业’; 这个 sql 语句得来的, 可以把这个语句看做一个整体, 替换到 1 这个位置上, 这两个 sql 合并成一个 sql 就是子查询
4.2.5. 合并查询
把两个查询的结果集合,合并成一个集合
使用 union 关键字来完成
格式比如: select 1 union select 2
要求两个 select 查询的结果集, 列数和类型要匹配, 列名不影响,最终的列名就是第一个 select 的列名
例题: 查询 id 小于 3, 或者名字为 ‘英文’ 的课程
如果都是查询同一个表,可以用 or 解决,但是 union 左右两侧 sql 可以查询两个不同的表, or 可不行
union 还会自动对查询结果进行去重
union all 不会去重