SQL进阶理论篇(二):数据库的设计范式

文章目录

  • 简介
  • 数据库的设计范式有哪些
  • 数据库中的几种键
  • 从1NF到3NF
    • 1NF
    • 2NF
    • 3NF
    • BCNF(巴斯范式)
  • 反范式设计
  • 反范式的适用场景
  • 总结
  • 参考文献

简介

本小节主要内容:

  • 数据库的设计范式都有哪些
  • 数据库的键都有哪些
  • 1NF、2NF和3NF都是指什么?
  • 什么是BCNF
  • 什么是反范式设计,它有什么适用场景?

数据库的设计范式有哪些

范式(NF),可以理解成是一张数据表在设计时需要满足的某种设计标准的级别。

目前,关系型数据库一共有六种范式,按照范式级别,从低到高依次是:

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

数据库的范式设计越高阶,冗余度就越低 ,同时,高阶范式一定满足低阶范式的要求,比如,满足2NF的一定满足1NF。

一般来讲,在关系型数据库里,数据表的设计应该尽量满足3NF,但是并不绝对,很多时候,出于提升查询性能的需要,我们会反范式设计,主动冗余一些字段。

SQL进阶理论篇(二):数据库的设计范式_第1张图片

数据库中的几种键

超键:只要是能唯一标识元组的属性集,都叫做超键。

候选键:是不含多余属性的超键,可以理解成是最小超键,从它的集合里找不到一个子集也是超键;

主键:从候选键中选择一个作为主键;

外键:不介绍了

主属性:包含在任一候选键里的属性,称为主属性;

非主属性:与主属性相对,不包含在任何一个候选键里的属性。

举个例子,以球员表为例,一个球员表包含球员的编号、姓名、证件号、年龄和球队编号。

而超键,就是包含球员编号或者证件号的任意组合,比如说(球员编号)、(证件号)、(证件号、姓名)、(证件号、姓名、年龄)等。

而候选键就是最小超键,即(球员编号)或者(证件号)。我们可以从它俩里选一个做主键。

外键就是球队编号。

主属性就是球员编号、证件号,剩下的都是非主属性。

从1NF到3NF

1NF

1NF是指数据库表中的任何属性都是原子性的,不可再分。事实上,任何DBMS都会满足第一范式的要求,不会将字段进行拆分。

2NF

2NF,指数据库表中的非主属性,都要和这个数据表的候选键有完全依赖关系。这里的完全依赖是指,依赖候选键的全部属性。

简单的说呢,就是针对概念上的联合主键,非主属性依赖于联合主键的每一个字段,而非部分字段

接下来,我们介绍一个没有满足2NF的例子。

我们设计了一张球员比赛表,其包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地这些属性(字段)。

按照上一小节介绍的,这张表的候选键和主键都是(球员编号,比赛编号),因此我们可以得到以下关系:

(球员编号,比赛编号)决定了(姓名、年龄、比赛时间、比赛场地)

这个是不满足2NF的。

为啥这么说呢?

因为非主属性(姓名、年龄、比赛时间、比赛场地)并没有完全依赖主属性。

这张表里还存在以下关系:

(球员编号) 决定了 (姓名,年龄)
(比赛编号) 决定了 (比赛时间,比赛场地)

候选键的某个字段就可以决定非主属性。

这会产生什么问题呢?

  • 数据冗余:如果一个球员参加了m场比赛,那么球员的年龄和姓名数据就重复了m-1次。如果一场比赛有n个球员参加,那么比赛的时间和场地信息就重复了n-1次。
  • 插入异常:如果我们想添加一场新的比赛,但是还没有决定哪些球员能参加,这个数据是没法插入的;
  • 删除异常:如果我想删除某场比赛的信息,但是有球员只参加了这一场比赛的话,就会把这个球员的信息也给删掉;
  • 更新异常:如果一场比赛调整了比赛时间,那么我需要把这个表里这场比赛的全部记录都update时间。

为了避免以上情况,我们可以按照2NF,将球员比赛表设计为三张表:

  • 球员表,包含球员编号,姓名,年龄;
  • 比赛表,包含比赛编号、比赛时间、比赛场地;
  • 球员比赛表,包含比赛编号、球员编号和个人得分等各种字段。

所以,从某种程度上来讲,2NF是1NF的升级。1NF只告诉我们字段属性是独立的,而2NF告诉我们一张表就是一个独立的对象,即==一张表只表达一个意思==。

3NF

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

在3NF里,每个非主属性,必须且只能直接依赖于全部候选键。可以理解成,非主属性只能直接依赖于全部主键字段。

再举一个例子,假设我们有这么一张球员表,其包含了球员编号、球员姓名、球队名称和球队教练。

这些字段的依赖关系如下图:

SQL进阶理论篇(二):数据库的设计范式_第2张图片

可以看到,球队教练这个属性,是传递依赖于球员编号的,这个就不符合3NF的要求。

如果要按照3NF来设计的话,上面应该分成两个表:

  • 球员表:球员编号、姓名和球队编号
  • 球队表:球队编号、球队名称和球队教练;

BCNF(巴斯范式)

举一个例子,假设我们有一张仓库管理表,包含以下字段:

SQL进阶理论篇(二):数据库的设计范式_第3张图片

在这个表里,我们指定一个仓库只能有一个管理员,且一个管理员只能管理一个仓库,即仓库和管理员严格一对一。

这样的话,仓库名决定了管理员,而管理员也决定了仓库名,同时,(仓库名,物品名)或者是(管理员,物品名)又决定了数量这个属性。

所以,这张表的候选键就是(仓库名,物品名)、(管理员,物品名)。我们随便挑一个候选键作为主键,比如(仓库名,物品名)。

于是,主属性就是仓库名、物品名、管理员,非主属性就是数量。

接下来,我们判断一下这张表的所属范式。

数据表每个属性都是原子性的,符合1NF的要求;

非主属性"数量"与候选键全部依赖,不存在仅使用候选键的某个子集就可以决定数量的情况。比如说仓库名或者物品名都无法决定数量。所以符合2NF的要求;

非主属性"数量"不传递依赖于候选键,而且也没有其他非主属性让它去传递依赖。所以符合3NF的要求。

综上,数据表符合3NF的要求,那是不是在设计上就没什么问题了呢?

不是,还是有很多问题:

  • 增加一个新仓库,但是没有存放任何物品。由于物品名是联合主键,不能为空,因此插入异常;
  • 仓库要更换管理员,需要修改这个仓库下所有物品的记录;
  • 仓库的商品卖空了,没有物品了。由于主键不能为空,这个仓库的信息只能删除,那么这个仓库的信息和对应的管理员信息也会被删除掉。

可以看到,即使一张表满足了3NF的要求,同样还是存在插入、更新和删除的问题。

这是为什么呢?

归根结底的原因是,主属性"仓库名"对候选键(管理员,物品名)是部分依赖的关系。“管理员"可以单独决定"仓库名”。

于是,人们在3NF的基础上进行了改进,提出了BCNF。

BCNF在3NF的基础上,进一步要求,主属性也不能对候选键有部分依赖或者传递依赖。

因此,根据BCNF,我们可以将上面的仓库管理表进一步细分成两张表:

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

反范式设计

越高阶的范式,设计出来的表数量就越多,数据冗余度就越低。但是这其实并不完全是好事,因为分化出来的表越多,意味着我们要做一个业务查询的时候,需要关联的表就越多。

因此,我们在实际生产中,经常为了性能和读取效率而做反范式的设计。即允许少量冗余字段的存在,以空间来换时间。

反范式,也是一种对查询效率的优化思路。

接下来,举一下教程里的例子,通过实验来模拟一下反范式的优化效果。

我们需要两张表

商品评论表,product_comment,对应字段如下:

在这里插入图片描述

用户表,user,对应的字段如下:

在这里插入图片描述

然后我们分别给两张表模拟出百万量级的数据,可以通过存储过程来实现。

给用户表随机生成100w用户:

CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_user`(IN start INT(10), IN max_num INT(10))
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE date_start DATETIME DEFAULT ('2017-01-01 00:00:00');
DECLARE date_temp DATETIME;
SET date_temp = date_start;
SET autocommit=0;
REPEAT
SET i=i+1;
SET date_temp = date_add(date_temp, interval RAND()*60 second);
INSERT INTO user(user_id, user_name, create_time)
VALUES((start+i), CONCAT('user_',i), date_temp);
UNTIL i = max_num
END REPEAT;
COMMIT;
END

在循环前,我们将 autocommit 设置为 0,这样等计算完成再统一插入,执行效率更高。

然后调用call insert_many_user(10000, 1000000);,生成编号从10000开始的百万用户数据,教程里是消耗了1分37秒。

接着给商品评论表随机生成100w条评论,内容为20个随机字母,存储过程如下:

CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_product_comments`(IN START INT(10), IN max_num INT(10))
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE date_start DATETIME DEFAULT ('2018-01-01 00:00:00');
DECLARE date_temp DATETIME;
DECLARE comment_text VARCHAR(25);
DECLARE user_id INT;
SET date_temp = date_start;
SET autocommit=0;
REPEAT
SET i=i+1;
SET date_temp = date_add(date_temp, INTERVAL RAND()*60 SECOND);
SET comment_text = substr(MD5(RAND()),1, 20);
SET user_id = FLOOR(RAND()*1000000);
INSERT INTO product_comment(comment_id, product_id, comment_text, comment_time, user_id)
VALUES((START+i), 10001, comment_text, date_temp, user_id);
UNTIL i = max_num
END REPEAT;
COMMIT;
END

然后调用call insert_many_product_comments(10000, 1000000),教程里是花了2分7秒。

如果我们现在接到一个需求,需要查询产品10001的前1000条评论,并且需要显示出对应的评论人姓名,需要写成:

SELECT p.comment_text, p.comment_time, u.user_name FROM product_comment AS p 
LEFT JOIN user AS u 
ON p.user_id = u.user_id 
WHERE p.product_id = 10001 
ORDER BY p.comment_id DESC LIMIT 1000

运行时长为0.395s。

效率不高。这是因为两个表都是百万量级的大表,关联的时候要做嵌套查询,消耗自然就上去了。

如果我们想提升查询的效率,就可以允许适当的字段冗余,比如说在商品评论表里增加用户名这个字段,这样一来,只需要单表查询就可以完成预定的需求,如:

SELECT comment_text, comment_time, user_name FROM product_comment2 WHERE product_id = 10001 ORDER BY comment_id DESC LIMIT 1000

消耗时间仅为0.039s。

反范式的适用场景

综上,反范式可以通过空间来换时间,从而提升查询效率。

但是在数据量小的情况下,反范式并体现不出优势,反而会把数据库的搞得更加复杂,比如说增加了更新的难度,可能要单独编写触发器之类的,自动更新。

反范式经常被用在数据仓库的设计之中。因为数仓通常是存储历史数据,以OLAP为主,对增删改的实时要求不高,反而是对历史数据的分析需求强。这时候适当增加数据的冗余度,更方便进行数据分析。

教程里,对下数据仓库和数据库在使用上的区别,是这么总结的:

  • 数据库设计的目的在于捕获数据,而数据仓库设计的目的在于分析数据;

  • 数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据;

  • 数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计。

其实上面的总结里还是保守了,数仓对反范式的应用是很广泛的。比如说数仓中,为了方便分析,经常会加工一些"宽表",冗余大量字段,来实现单表支撑分析的功能。另外,在数仓的分层里,从ODS到ADS,上层其实都是对下层的冗余。

总结

可以看到,从1NF到BCNF,突出的都是一个去冗余化,主打的就是一个精简,就如同"相同的代码不能出现两次",在范式设计的思想下,会重复出现的字段都应该抽离出来单独成表。

或者可以换个角度去思考,以BCNF一节中的例子来说,这个表里其实是有3个实体的,分别是仓库、管理员和物品。其中仓库:管理员是1:1,而仓库:物品是N:1,管理员:物品同样是N:1。

三个实体,且三个实体间是1:1:N。这就不符合范式设计的要求了,在范式设计下,单表里最好只保留两个及以下的实体,不同实体之间的关系最好是1:1或者1:N。因此优化后的例子里,把1:1:N拆分成了1:1和1:N两张表。

当然,实际设计数据表的时候,未必要符合这些原则,尤其是是在分布式的OLAP应用场景下。

一方面是这些范式本身就存在一些问题,在插入、更新和删除的时候可能会带来一些异常。另外,它们也会降低查询的效率,这是因为范式等级越高,设计出来的数据表就越多,实际做查询的时候就需要关联很多很多张表,从而降低查询效率。

说句题外话,我在教程的下面评论里看到一条极其有才的评论:

范式与反范式,正如传统与解构,规则与务实,稳定与突破,守成与创新,是阴阳动静的矛盾关系,两者一而二,二而一,即和而不同、求同存异,落脚点是务实,也就是应用场景和业务需求。
所以说,这已经不单是数据库设计的问题,而中国哲学体系在互联网商业中实践指导。
数据库设计提出范式的同时存在反范式的要求,符合否定之否定的螺旋上升轨迹,是数据库也是SQL语言保持强壮生命力而经久不衰的重要原因,是现实生存逻辑的映射。

看完当场下跪,这就是互联网的发言吗。

参考文献

  1. 21丨范式设计:数据表的范式有哪些,3NF指的是什么?
  2. 22丨反范式设计:3NF有什么不足,为什么有时候需要反范式设计?

你可能感兴趣的:(#,SQL基础,数据库,sql,database)