Phase2 Day19 数据库范式设计 & 复杂查询

数据库范式设计

范式是数据表设计的基本原则,又很容易被忽略。很多时候,当数据库运行了一段时间之后,我们才发现数据表设计得有问题。重新调整数据表的结构,就需要做数据迁移,还有可能影响程序的业务逻辑,以及网站正常的访问。所以在开始设置数据库的时候,我们就需要重视数据表的设计.

范式级别

我们在设计关系型数据库模型的时候,需要对关系表各个字段之间联系的合理化程度进行定义,这就有了不同等级的规范要求,这些规范要求被称为范式(NF)。你可以把范式理解为,一张关系表的设计结构需要满足的某种设计标准的级别

  • 1NF(第一范式)
  • 2NF(第二范式)
  • 3NF(第三范式)
  • BCNF(巴斯 - 科德范式)
  • 4NF(第四范式)
  • 5NF(第五范式,又叫做完美范式)

数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,比如满足 2NF的一定满足 1NF,满足 3NF 的一定满足 2NF,依次类推
一般来说数据表的设计应尽量满足 3NF。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反范式设计

数据表中的键

范式的定义会使用到主键和候选键(因为主键和候选键可以唯一标识元组),数据库中的键(Key)由一个或者多个属性组成

  • 超键:能唯一标识元组的属性集叫做超键。
  • 候选键:如果超键不包括多余的属性,那么这个超键就是候选键。
  • 主键:用户可以从候选键中选择一个作为主键。
  • 外键:如果数据表 R1 中的某属性集不是 R1 的主键,而是另一个数据表 R2 的主键,那么这个属性集就是数据表 R1 的外键。
  • 主属性:包含在任一候选键中的属性称为主属性。
  • 非主属性:与主属性相对,指的是不包含在任何一个候选键中的属性

简单的举个栗子:NBA球员表和球队表。

球员表:球员编号、姓名、身份证号、年龄和球队编号;
球队表:球队编号、主教练和球队所在地。
对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号)(球员编号,姓名)(身份证号,年龄)等。候选键就是最小的超键,对于球员表来说,候选键就是(球员编号)或者(身份证号)。主键是我们自己选定,也就是从候选键中选择一个,比如(球员编号)。外键就是球员表中的球队编号。在球员表中,主属性是(球员编号)(身份证号),其他的属性(姓名)(年龄)(球队编号)都是非主属性

从1NF到3NF

  • 1NF 指的是数据库表中的任何属性都是原子性的,不可再分。不同的业务,对不可再分的要求也不一样
  • 2NF 指的数据表里的非主属性都要和这个数据表的候选键有完全依赖关系.所谓完全依赖不同于部分依赖,也就是不能仅依赖候选键的一部分属性,而必须依赖全部属性

举个栗子:比如我们设计一张球员比赛表。
球员比赛表:球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地,得分。
这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键来决定如下的关系:(球员编号,比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分)
但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:
(球员编号) → (姓名,年龄), (比赛编号) → (比赛时间, 比赛场地)也就是说候选键中的某个字段决定了非主属性。你也可以理解为,对于非主属性来说,并非完全依赖候选键

这样会产生怎样的问题呢?

  1. 数据冗余:如果一个球员可以参加 m 场比赛,那么球员的姓名和年龄就重复了 m-1 次。一个比赛
    也可能会有 n 个球员参加,比赛的时间和地点就重复了 n-1 次。
  2. 插入异常:如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。
  3. 删除异常:如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。
  4. 更新异常:如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况

为了避免出现上述的情况,我们可以把球员比赛表设计为下面的三张表。

  • 球员表包含球员编号、姓名和年龄等属性;
  • 比赛表包含比赛编号、比赛时间和比赛场地等属性;
  • 球员比赛关系表包含球员编号、比赛编号和得分等属性。

这样的话,每张数据表都符合第二范式,也就避免了异常情况的发生。某种程度上 2NF 是对 1NF 原子性的升级。1NF 告诉我们字段属性需要是原子性的,而 2NF 告诉我们一张表就是一个独立的对象,也就是说一张表只表达一个意思

3NF 在满足 2NF 的同时,对任何非主属性都不传递依赖于候选键。也就是说不能存在非主属性 A 依赖于非主属性 B,非主属性 B 依赖于候选键的情况

我们用球员表举例子,这张表包含的属性包括球员编号、姓名、球队名称和球队主教练。现在,我们把属性之间的依赖关系画出来,如下图所示:
在这里插入图片描述
你能看到球员编号决定了球队名称,同时球队名称决定了球队主教练,非主属性球队主教练就会传递依赖于球员编号,因此不符合 3NF 的要求。

如果要达到 3NF 的要求,需要把数据表拆成下面这样:球员表的属性包括球员编号、姓名和球队名称;球队表的属性包括球队名称、球队主教练

BCNF

如果数据表的关系模式符合 3NF 的要求,就不存在问题了吗?我们来看下这张仓库管理关系表:
在这里插入图片描述
在这个数据表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。我们先来梳理下这些属性之间的依赖关系。

仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个属性。这样,我们就可以找到数据表的候选键是(管理员,物品名)和(仓库名,物品名)。

然后我们从候选键中选择一个作为主键,比如(仓库名,物品名)。在这里,主属性是包含在任一候选键中的属性,也就是仓库名,管理员和物品名。非主属性是数量这个属性。

那如何判断这张表的范式等级呢?
首先,数据表每个属性都是原子性的,符合 1NF 的要求;其次,数据表中非主属性”数量“都与候选键全部依赖,(仓库名,物品名)决定数量,(管理员,物品名)决定数量,因此,数据表符合 2NF 的要求;最后,数据表中的非主属性,不传递依赖于候选键。因此符合 3NF 的要求。

既然这张表满足了 3NF 的要求,那么它就不存在问题了吗?我们来分析下面的情况:

  1. 增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此
    会出现插入异常;
  2. 如果仓库更换了管理员,我们就可能会修改数据表中的多条记录;
  3. 如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。

所以,即便数据表符合 3NF 的要求,同样可能存在插入,更新和删除数据的异常情况

那肿么办呢?

首先我们分析下造成这些异常的原因:主属性管理员对于候选键(仓库名,物品名)是部分依赖的关系,这样就有可能导致上面的异常情况。
因此,人们在 3NF 的基础上进行了改进,提出了 BCNF。它在 3NF 的基础上消除了主属性对候选键的部分依赖或者传递依赖关系。

为了满足 BCNF 的要求,我们需要将上面的仓库管理表拆分成下面两张表:

仓库表:仓库名, 管理员。
库存表:仓库名,物品名,数量。

这样就不存在主属性对于候选键的部分依赖或传递依赖

总结

  • 1NF 需要保证表中每个属性都保持原子性;
  • 2NF 需要保证表中的非主属性与候选键完全依赖;
  • 3NF 需要保证表中的非主属性与候选键不存在传递依赖。
  • BCNF 需要保证表中的主属性与候选键不存在部分依赖或者传递依赖

复杂查询

1. 链接查询

  • 在设计表的时候,为了避免数据的冗余,我们往往会将数据分散到多个表中。因此,在我们查询数据的时候,需要连接多个表进行查询
  • SQL92 和 SQL99 连表查询的语法有很大的不同。建议大家采用 SQL99 标准,因为它的层次性更强,可读性也更高。我们也以 SQL99 标准进行讲解。
  • 连接查询大致可以分为 5 种,分别为:交叉连接,等值连接,非等值连接,外连接和自连接
1.1 交叉连接

交叉连接也叫笛卡尔乘积。那什么是笛卡尔乘积呢?wikipedia 对笛卡尔乘积的定义如下:

在数学中,两个集合X和Y的笛卡儿积(英语:Cartesian product),又称直积,在集合论中表示为X×Y,是所有可能的有序对组成的集合,其中有序对的第一个对象是X的成员,第二个对象是Y的成员。

在 SQL99 中,我们可以通过 CROSS JOIN 获取多张表的笛卡尔乘积。

create database nba;
use nba;
show tables;
select * from player;
select * from team;
select * from player cross join team
#team_id不会合并

在这里插入图片描述

交叉连接只是获取所有数据的集合,其中大多数数据都是没意义的,所以我们还需要对数据进行筛选。交叉连接是所有其它连接的基础

1.2 等值连接
  • 等值连接就是对多张表中相同的字段进行等值判断。在 SQL99 中可以有多种方式表示等值连接
1.2.1 自然连接
  • NATURAL JOIN 会自动帮你查询两张连接表中所有相同的字段,然后进行等值连接
# a. 自然连接 
select * from player natural join team;//会自动筛选team相等的元素
#team_id会合并

在这里插入图片描述

1.2.2 USING连接
  • 还可以用 USING 来指定用哪些同名字段进行等值连接
# b. using连接
select * from player join team using(team_id);
#team_id会合并

在这里插入图片描述

1.2.3 ON连接
  • ON 表示我们想要连接的条件,我们也可以用 ON 来实现等值连接
  • 可以实现等值连接,也可以实现非等值连接
# c. on连接 
select * from player join team on player.team_id = team.team_id;
#team_id不会合并

在这里插入图片描述

1.3 非等值连接
  • 连接两张表的条件如果是相等判断,那就是等值连接,否则就是非等值连接。

比如说:我们想查询每个球员的身高级别

select * from player;
select * from height_grades;

select * from player cross join height_grades;

select player_id, player_name, height, height_level
from player join height_grades
on height between height_lowest and height_highest; 
1.4 外连接
  • 外连接除了查询满足条件的记录以外,还可以查询某一方不满足条件的记录。两张表做外连接,会有一张表是主表,另一张表是从表
1.4.1左外连接
  • 左外连接,就是左边的表是主表,需要显示左边表的全部行。右边表是从表,只显示满足条件的行。关键字为 LEFT OUTER JOIN
create database mydb1;
use mydb1;
create table girls (
	id int primary key auto_increment,
    name varchar(255),
	bf_id int
);
insert into girls values (null, 'Allen', 1);
insert into girls values (null, 'Beyonce', 2);
insert into girls values (null, 'Cindy', 100);
insert into girls values (null, 'Diana', null);

create table boys (
	id int primary key auto_increment,
    name varchar(255),
	gf_id int
);
insert into boys values (null, 'Algebra', 1);
insert into boys values (null, 'Bill', 2);
insert into boys values (null, 'Chris', 100);
insert into boys values (null, 'David', null);

select * from girls left join boys 
on girls.bf_id = boys.id;# 外连接,不满足的也会显示

select * from girls join boys#此处join实际为inner join
on girls.bf_id = boys.id;#内连接,不满足的不会显示	

在这里插入图片描述
在这里插入图片描述

1.4.2右外连接
  • 右外连接,就是右边的表是主表,需要显示右边表的全部行。左边表是从表,只显示满足条件的行。关键字为 RIGHT OUTER JOIN
select * from girls right join boys
on boys.gf_id = girls.id;

在这里插入图片描述

1.4.3全外连接
  • 两张表都是主表,都需要显示全部行。但是MySQL不支持全外连接。关键字为 FULL OUTER JOIN
# 全外连接
select * from girls full join boys
on boys.gf_id = girls.id;
1.5 自连接
  • 我们可以连接不同的表,也可以对同一张表进行连接,这样的连接我们称之为自连接。

比如我们想要查看比布雷克-格里芬高的球员都有谁

select * from player;
select a.player_id, a.player_name, a.height from player as a join player as b
on b.player_name = '布雷克-格里芬'
where a.height > b.height;

2.联合查询

  • 我们可以用 UNION 关键字,将多个结果集合并成一个结果集,这样的查询我们叫联合查询。
  • 应用场景: 要查询的结果来自多个表,且多个表没有直接的连接关系,但查询的信息一致时。
  • 注意事项:
    a. 列数一致
    b. 对应的数据最好一致
    c. UNION会去重, UNION ALL不会去重
select id, name from girls
union
select id, name from boys;

# 用子查询 
select * from player as a
where height > (select height from player as b where player_name = '布雷克-格里芬');

3.子查询

  • 子查询其实就是嵌套在查询中的查询。这样做的好处是:我们可以进行更加复杂的查询,更容易理解查询的过程。很多情况下,我们无法直接从数据表中得到我们想要的结果。往往需要先进行一次查询,然后在这次查询的基础上,再次进行查询

子查询可以分为关联子查询非关联子查询

如果子查询只执行一次,然后子查询的结果集会作为外部查询的条件进行判断,那么这样的子查询叫做非关联子查询。比如:我们想要查询哪个球员的身高最高,最高身高是多少

use nba;
# 子查询 
# 非关联子查询 
# 比如:我们想要查询哪个球员的身高最高,最高身高是多少?
# ① 查询最高身高是多少 
select max(height) from player;
# ② 查询身高等于最高升高的球员 
select player_id, player_name, height from player
where height = (select max(height) from player);  # 标量子查询 scalar

如果子查询依赖于外部查询,通常情况下是因为子查询用到了外部查询的某些字段。因此,每执行一次外部查询,子查询都要重新执行一次,这样的子查询叫做关联子查询。比如:我们想要查询每个球队中大于平均身高的球员有哪些,并显示球员姓名,身高以及所在球队 ID

# 关联子查询 
# 比如:我们想要查询每个球队中大于平均身高的球员有哪些,
# 并显示球员姓名,身高以及所在球队 ID。
select player_name, height, team_id from player as a
where height > (select avg(height) from player as b where b.team_id = a.team_id);
3.1 EXISTS 子查询
  • 关联子查询可能会搭配 EXISTS 关键字一起使用。 EXISTS 用来判断子查询的结果集是否为空集。如果不为空集返回 True ,如果为空集返回 False
# Exists
# 举个列子:查询出过场的球员都有哪些,并显示他们的球员ID,球员姓名,球队ID。
# ① 涉及哪些表? 
# select * from player;
# select * from player_score;
select player_id, player_name, team_id 
from player
where exists (
	select player_id 
    from player_score 
    where player_score.player_id = player.player_id
);
  • 那么, NOT EXISTS 自然就是不存在的意思。比如:查询没出过场的球员都有哪些,并显示他们的球员ID,球员姓名,球队ID
# 比如:查询没出过场的球员都有哪些,并显示他们的球员ID,球员姓名,球队ID。
select player_id, player_name, team_id 
from player
where not exists (
	select player_id 
    from player_score 
    where player_score.player_id = player.player_id
);
3.2 集合比较子查询

集合比较子查询的作用是与外部查询的结果集进行比较。主要有以下几个关键字:IN, SOME (ANY),ALL。他们的含义如下:
在这里插入图片描述

# IN 
# 查询出过场的球员都有哪些,并显示他们的球员ID,球员姓名,球队ID。
select player_id, player_name, team_id 
from player
where player_id in (
	select distinct player_id 
    from player_score
);

了解了 IN 关键字后,我们接下来看下 SOME 和 ALL 。它们都需要和比较操作符一起使用,这些比较操作符包括: > , = , < , >= , <= , 和 <> 。

举个例子:我们要查询比印第安纳步行者 (team_id=1002) 中某个球员身高高的球员有哪些,显示它们的球员ID,球员姓名和球员身高。

# SOME(ANY)
# 我们要查询比印第安纳步行者 (team_id=1002) 中某个球员身高高的球员有哪些,
# 显示它们的球员ID,球员姓名和球员身高。
select player_id, player_name, height
from player as a
where team_id != 1002 and height > SOME(
	select height
    from player as b
    where b.team_id = 1002
);


select player_id, player_name, height
from player as a
where team_id != 1002 and height > (
	select min(height)
    from player as b
    where b.team_id = 1002
);

同样,如果我们想查询比印第安纳步行者 (team_id=1002) 中所有球员身高都高的球员有哪些,显示它们的球员ID,球员姓名和球员身高

# 同样,如果我们想查询比印第安纳步行者 (team_id=1002) 中所有球员身高都高的球员有哪些,
# 显示它们的球员ID,球员姓名和球员身高.
select player_id, player_name, height
from player as a
where height > ALL(
	select height
    from player as b
    where b.team_id = 1002
);


select player_id, player_name, height
from player as a
where height > (
	select max(height)
    from player as b
    where b.team_id = 1002
);

最后,再强调下 SOME 和 ALL 必须要与一个比较操作符一起使用,不然起不到集合比较的作用

3.3 子查询作为计算字段

子查询甚至可以作为计算字段存在。举个例子:查询每个球队的名称,和它们的球员数

# 子查询作为计算字段存在 
# 查询每个球队的名称,和它们的球员数
# ① 涉及哪些表? team, player
select team_id, team_name, (
	select count(*) 
    from player 
    where player.team_id = team.team_id) as player_num
from team;

# 连接查询 
# ① 查询每个球队的球员数目 
  select team_id, count(*) from player group by team_id;
  
# ② 连接team表查询 
  select * 
  from team	
	left join (
	select team_id, count(*) as player_num from player group by team_id) as tmp
  using(team_id);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值