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

文章目录

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

简介

本小节主要内容:

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

数据库的设计范式有哪些

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

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

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

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

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

在这里插入图片描述

数据库中的几种键

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

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

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

外键:不介绍了

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

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

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

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

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

外键就是球队编号。

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

从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里,每个非主属性,必须且只能直接依赖于全部候选键。可以理解成,非主属性只能直接依赖于全部主键字段。

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

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

在这里插入图片描述

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

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

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

BCNF(巴斯范式)

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

在这里插入图片描述

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

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

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

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

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

数据表每个属性都是原子性的,符合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有什么不足,为什么有时候需要反范式设计?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/243224.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Dockerfile创建LNMP

实验组件 172.111.0.10:nginx docker-nginx 172.111.0.20:mysql docker-mysql 172.111.0.30:php docker-php 实验步骤 1.建立nginx-lnmp镜像及容器 cd /opt mkdir nginx cd nginx/ --上传nginx-1.22.0.tar.gz和wordpress-6.4.2-zh_C…

【LeetCode每日一题】1904. 你完成的完整对局数

给你两个字符串 startTime 和 finishTime ,均符合 "HH:MM" 格式,分别表示你 进入 和 退出 游戏的确切时间,请计算在整个游戏会话期间,你完成的 完整对局的对局数 。 如果 finishTime 早于 startTime ,这表示…

欧拉函数与欧拉定理

文章目录 AcWing 873. 欧拉函数题目链接欧拉函数欧拉函数的证明思路CODE时间复杂度分析 AcWing 874. 筛法求欧拉函数题目链接问题分析与时间复杂度CODE思路 欧拉定理 AcWing 873. 欧拉函数 题目链接 https://www.acwing.com/activity/content/problem/content/942/ 欧拉函数 …

四六级高频词组7

目录 词组 其他文章链接: 词组 251. (be) equivalent to(equal in value, amount, meaning) 相等于, 相当于 252. in essence (in itsones nature) 本质上…

20、备忘录模式(Memento Pattern,不常用)

备忘录模式又叫作快照模式,该模式将当前对象的内部状态保存到备忘录中,以便在需要时能将该对象的状态恢复到原先保存的状态。 备忘录模式提供了一种保存和恢复状态的机制,常用于快照的记录和状态的存储,在系统发生故障或数据发生…

网络安全项目实战(三)--报文检测

6. TCP/IP协议栈及以太网帧 目标 了解TCP/IP协议栈的组织结构掌握以太网帧的数据格式定义能应用编码实现以太网帧的解析方法 6.1. TCP/IP 协议栈 TCP/IP网络协议栈分为应用层(Application)、传输层(Transport)、网络层&#xf…

【UML】第4篇 UML公共机制(补扩展机制)

目录 一、扩展机制 1.1 构造型 1.2 标记值(Tagged Value) 1.3 约束(Constraint) 上节扩展机制没有讲完,如上图。 一、扩展机制 1.1 构造型 UML中的扩展机制包括约束、构造型和标记值,其中的构造型定义…

yo!这里是Linux信号相关介绍

目录​​​​​​​ 前言 基本介绍 概念 信号列表 信号处理 产生(发送)信号 通过按键产生 系统函数产生 软件条件产生 硬件异常产生 阻塞信号 信号状态 sigset_t 状态相关函数 1.sigprocmask 2.sigpending 捕捉信号 内核态与用户态 捕捉过程 sigaction 后…

分库分表及ShardingShpere-proxy数据分片

为什么需要分库? 随着数据量的急速上升,单个数据库可能会QPS过高导致读写耗时过长而出现性能瓶颈,所以需要考虑拆分数据库,将数据库分布在不同实例上提升数据库可用性。主要的原因有如下: 磁盘存储。业务量剧增&…

nodejs项目设置全局变量(global)

文章目录 前言一、使用global二、解决type typeof globalThis has no index signature.ts问题1、新建 /types/global.d.ts文件2、或者直接在入口文件/src/index.ts定义 三、最终效果鼠标放在global上,可显示global的类型生效了~ ![在这里插入图片描述](https://img-…

I.MX RT1170双核学习(2):双核相互激活和启动流程

RT1170这个芯片带有双核:Cortex-M7和Corterx-M4,两个核都可以独立地运行,当然双核也可以同时运行。在上一篇文章中,介绍了一下在RT1170中消息模块MU的使用:双核通信之MU消息单元详解,因为这是双核之间用来通…

05 python数据容器

5.1 数据容器认识 5.2 python列表 5.2.1 列表的定义 演示数据容器之:list 语法:[元素,元素,....] #定义一个列表List List [itheima,uityu,gsdfg] List1 [itheima,6666,True] print(List) print(List1) print(type(List)) pr…

smartKettle离线部署及问题记录

目录 📚第一章 前言📗背景📗目的📗总体方向 📚第二章 部署📗源码下载📗后端部署📕导入后端项目📕修改settings.xml(自动下载相关jar包)📕 编译📕 …

0x13 链表与邻接表

0x13 链表与邻接表 数组是一种支持随机访问,但不支持在任意位置插入和删除元素的数据结构。与之相对应,链表支持在任意位置插入或删除元素,但只能按顺序依次访问其中元素。我们可以使用一个struct来表示链表的节点,其中可以存储任…

MySQL线上死锁案例分析

项目场景 项目开发中有两张表:c_bill(账单表),c_bill_detail(账单明细表),他们的表结构如下(这里只保留必要信息): CREATE TABLE c_bill_detail (id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主…

Gin之GORM 查询语句

前期工作可以看之前的(连接数据库;以及确定要操作的库) Gin之GORM 操作数据库(MySQL)-CSDN博客https://blog.csdn.net/m0_72264240/article/details/134948202?spm1001.2014.3001.5502这次我们操作gin库下的另外一个…

Lenovo联想拯救者Legion Y9000X 2021款(82BD)原装出厂Windows10系统

链接:https://pan.baidu.com/s/1GRTR7CAAQJdnh4tHbhQaDQ?pwdl42u 提取码:l42u 联想原厂WIN10系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、联想电脑管家等预装程序 所需要工具:16G或以上的U盘 文件格式&am…

记录汇川:套接字TCP通信-梯形图

H5U集成一路以太网接口。使用AutoShop可以通过以太网方便、快捷对H5U进行行监控、下载、上载以及调试等操作。同时也可以通过以太网与网络中的其他设备进行数据交互。H5U集成了Modbus-TCP协议,包括服务器与客户端。可轻松实现与支持Modbus-TCP的设备进行通讯与数据交…

Redis哨兵模式:什么是哨兵模式、哨兵模式的优缺点、哨兵模式的主观下线和客观下线、投票选举、Redis 哨兵模式搭建

文章目录 什么是哨兵模式哨兵模式的优缺点主观下线和客观下线投票选举哨兵模式场景应用Redis version 6.0.5 集群搭建下载文件环境安装解压编译配置文件启动关闭密码设置 什么是哨兵模式 哨兵模式是Redis的高可用解决方案之一,它旨在提供自动故障转移和故障检测的功…

数据分析基础之《numpy(3)—基本操作》

一、基本操作 1、adarray.方法() 2、np.函数名() 二、生成数组的方法 1、生成0和1的数组 为什么需要生成0和1的数组? 我们需要占用位置,或者生成一个空的数组 (1)ones(shape[, dtype, order]) 生成一组1 shape:形…