什么是范式
为了建立冗余较小、结构合理的数据库,设计数据库时必须遵循一定的规则。在关系型数据库中,这种规则就是范式。范式是符合某一种级别的关系模式的集合。关系型数据库中的关系必须满足一定的要求,即满足不同的范式。
目前关系型数据库有六种范式,分别为:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、第四范式(4NF)、第五范式(5NF)和第六范式(6NF)。要求最低的范式是第一范式。第二范式在第一范式的基础上又进一步的添加了要求,其余范式依次类推。
一般说来,数据库只需满足第三范式就行了,而通常我们用的最多的就是第一范式、第二范式、第三范式,也就是接下来要讲的“三大范式”。
第一范式
第一范式(1NF)用来确保每列的原子性,要求每列(或者每个属性值)都是不可再分的最小数据单元(也称为最小的原子单元)。
举例:
员工编号 | 员工姓名 | 部门岗位 | 员工工资 |
---|---|---|---|
EM71001 | 张无忌 | 网络部软件开发工程师 | 8000 |
EM71002 | 楚天南 | 运营部运营总监 | 8100 |
EM71003 | 梁萧 | 销售部售货员 | 8300 |
上表中的部门岗位字段不符合第一范式,可以拆分为“部门”和“岗位”两个字段。
分解后方便我们对部门和岗位进行单独查询
员工编号 | 员工姓名 | 部门 | 岗位 | 员工工资 |
---|---|---|---|---|
EM71001 | 张无忌 | 网络部 | 软件开发工程师 | 8000 |
EM71002 | 楚天南 | 运营部 | 运营总监 | 8100 |
EM71003 | 梁萧 | 销售部 | 售货员 | 8300 |
不符合第一范式导致问题
- 部门和岗位作为一个表中一个字段进行设计,当我们需要对某个员工的部门或者岗位无法进行单独查询获取数据。
第二范式
在说明第二范式之前我们需要先了解什么是联合主键,什么是复合主键
什么是联合主键和复合主键
联合主键就是用2个或2个以上的字段组成主键。形式上复合主键就是联合主键,由多个字段作为主键,将记录形成一个唯一性。
场景1:
可以这么理解,比如,你的订单表里有很多字段,一般情况只要有个订单号bill_no做主键就可以了,但是,现在要求可能会有补 充订单,使用相同的订单号,那么这时单独使用订单号就不可以了,因为会有重复。那么你可以再使用个订单序列号bill_seq来 作为区别。把bill_no和bill_seq设成联合主键。即使bill_no相同,bill_seq不同也是可以的。alter table 表 add constraint pk_name primary key (列1,列2,…)
场景2:
联合主键通常用来表达多表之间的多对多的关系,比如老师(teacher)、学生(student)之间是多对多的关系,老师表和学生表之间就需要通过一个中间表来表达这种多对多的关系。
在这个中间表中就需要主键一个主键(mysql中不生成主键会怎么样)方便我们对表的查询,我们就可以将两个表中的编号字段拿到中间表中作为联合主键,学生编号字段中记录可以重复,老师编号字段中记录也可以重复,只要保障学生编号和老师编号两者一起唯一性即可,我们也不需要重新设置一个无关字段来作为主键增加数据的冗余性。
联合主键的好处是不需要因为需要主键而增加一个无用的主键列
例如如果不用联合主键你必须增加个列ID设置主键 ,但这个ID列 无任何作用 至于在什么情况下使用,就像刚才举例的,当你这个表的主键ID无任何用处,那么就用联合主键好了,你可以节约 一个列的空间,但如果这表的ID列要做为别的表的外键的话,就不能用联合主键了。
联合主键就是确定一条记录的唯一性啊
联合主键(复合主键)就是多个字段一起作为主键,mysql数据库会自动创建复合索引(联合索引),注意:多字段的联合索引需要同时创建,同时删除,否则报错Multiple primary key defined。当定义了一个主键,无法使用alter命令再添加一个主键,否则报错Multiple primary key defined,需要删除原来主键(alter table 表名 drop primary key),将多个字段同时设置为主键
什么是第二范式
第二范式(2NF)在第一范式的基础上更进一层,要求表中的每列都和主键相关,即要求实体的唯一性。如果一个表满足第一范式,并且除了主键以外的其他列全部都依赖于该主键,那么该表满足第二范式。
在满足第一范式基础上要求每个字段都和主键完整相关,而不是仅和主键部分相关(主要针对联合主键而言
订单编号 | 产品编号 | 产品价格 | 产品数量 | 订单金额 | 下单时间 |
---|---|---|---|---|---|
DN001 | CN5778 | 100 | 2 | 200 | 2019.12.3 |
DN002 | CN5778 | 120 | 4 | 480 | 2020.11.3 |
“订单详情表”使用“订单编号”和“产品编号”作为联合主键。此时“产品价格”、“产品数量”都和联合主键整体相关,但“订单金额”和“下单时间”只和联合主键中的“订单编号”相关,和“产品编号”无关。所以只关联了主键中的部分字段,不满足第二范式。
把“订单金额”和“下单时间”移到订单表就符合第二范式了。
订单编号 | 产品编号 | 产品价格 | 产品数量 |
---|---|---|---|
DN001 | CN5778 | 100 | 2 |
DN002 | CN5778 | 120 | 4 |
订单编号 | 订单金额 | 下单时间 |
---|---|---|
DN001 | 200 | 2019.12.3 |
DN002 | 480 | 2020.11.3 |
主键与外键在多表中的重复出现不属于数据冗余,非键字段的重复出现才是数据冗余。
第二范式避免了数据冗余和减少插入异常。
第三范式
第三范式(3NF)在第二范式的基础上更进一层,第三范式是确保每列都和主键列直接相关,而不是间接相关,即限制列的冗余性。如果一个关系满足第二范式,并且除了主键以外的其他列都依赖于主键列,列和列之间不存在相互依赖关系,则满足第三范式。
员工编号 | 员工姓名 | 部门 | 岗位 | 员工工资 |
---|---|---|---|---|
EM71001 | 张无忌 | 网络部 | 软件开发工程师 | 8000 |
EM71002 | 楚天南 | 运营部 | 运营总监 | 8100 |
EM71003 | 梁萧 | 销售部 | 售货员 | 8300 |
上面表中的“部门名称”和“员工编号”的关系是“员工编号”→“部门编号”
→“部门名称”,不是直接相关。此时会带来下列问题:
- 数据冗余:“部门名称”多次重复出现。
- 插入异常:组建一个新部门时没有员工信息,也就无法单独插入部门信息。就算强行插入部门信息,员工中没有员工信息的记录同样是非法记录。
- 删除异常:删除员工信息会连带删除部门信息导致部门信息意外丢失。
- 更新异常:哪怕只修改一个部门的名称也要更新多条员工记录。
正确的做法是:把上表拆分成两张表,以外键形式关联
员工编号 | 员工姓名 | 部门 | 岗位 | 员工工资 |
---|---|---|---|---|
EM71001 | 张无忌 | 001 | 软件开发工程师 | 8000 |
EM71002 | 楚天南 | 002 | 运营总监 | 8100 |
EM71003 | 梁萧 | 003 | 售货员 | 8300 |
员工编号 | 部门 |
---|---|
001 | 网络部 |
002 | 运营部 |
003 | 销售部 |
“部门编号”和“员工编号”是直接相关的。
第二范式的另一种表述方式是:两张表要通过外键关联,不保存冗余字段。例
如:不能在“员工表”中存储“部门名称”。
反范式化~突破范式
规则的变通
我们需要知道对于项目的最终用户来说,用户关心的是方便,清晰的数据结果。所以在设计数据库时,设计人员和客户在数据库的设计规范化和性能之间会有一定的矛盾。三大范式是设计数据库表结构的规则约束,但是在实际开发中允许局部变通。
比如为了快速查询到关联数据可能会允许冗余字段的存在。例如在员工表中存储部门名称虽然违背第三范式,但是免去了对部门表的关联查询。
根据业务功能设计数据库表
1、看得见的字段
能够从需求文档或原型页面上直接看到的数据都需要设计对应的数据库表、字段来存储。
比如根据设计页面可以知道用户信息需要包含如下字段:
- 账号
- 密码
- 名称
- 邮箱地址
2、看不见的字段
除了能够直接从需求文档中看到的字段,实际开发中往往还会包含一些其他字段来保存其他相关数据。
例如:管理员表需要再增加如下字段以有利于数据维护
- 主键
- 创建时间
- 冗余字段
为了避免建表时考虑不周有所遗漏,到后期再修改表结构非常麻烦,所以也有的团队会设置一些额外的冗余字段备用。
反范式化优缺点
优点:
- 可以减少表的关联
- 可以更好的进行索引优化
缺点:
- 数据表存在数据冗余及数据维护异常
- 对数据的修改需要更多的成本
数据库范式的优缺点
优点
- 减少数据冗余
- 消除异常(插入异常,更新异常,删除异常)
缺点
- 查询时需要联合多表查询,增加了查询的复杂度
- 查询时需要联合多个表,降低了数据库查询性能
总结
由上可以看出,为了满足三大范式,我们的数据操作性能会受到相应的影响。
对于应用数据库范式进行分解的过程中不难看出,应用的范式登记越高,则表越多。表多会带来很多问题;所以不是应用的范式越高越好,要看实际情况而定。第三范式已经很大程度上减少了数据冗余,并且减少了造成插入异常,更新异常,和删除异常了。我个人观点认为,大多数情况应用到第三范式已经足够,在一定情况下第二范式也是可以的。
补充:MySQL InnoDB数据表缺少主键会怎样—知乎(https://zhuanlan.zhihu.com/p/98084061)
问题
MySQL数据表使用InnoDB作为存储引擎的时候,数据结构就是使用B+树,而数据本身存储在主键索引上,也就是通常所说的聚簇索引,也就是每个表都需要有个聚簇索引树,但是,在建表的时候却发现可以不用指定主键,那么MySQL对于没有指定主键的表示如何处理的呢?
InnoDB索引
对于InnoDB,可以简单地把所有数据视为索引,每一个索引都对应一个B+数,而主键对应的索引就是聚簇索引,表的所有数据都存储在聚簇索引上,而除了聚簇索引的普通索引存储的只是主键的引用,所以,查询的时候对于普通索引需要进行回表才能取到具体数据。
缺少主键MySQL如何处理
既然InnoDB对数据的存储必须依赖于主键,那么对于没有创建主键的表,该怎么办?
InnoDB对聚簇索引处理如下: – 如果定义了主键,那么InnoDB会使用主键作为聚簇索引 – 如果没有定义主键,那么会使用第一非空的唯一索引(NOT NULL and UNIQUE INDEX)作为聚簇索引 – 如果既没有主键也找不到合适的非空索引,那么InnoDB会自动生成一个不可见的名为ROW_ID的列名为GEN_CLUST_INDEX的聚簇索引,该列是一个6字节的自增数值,随着插入而自增
很明显,缺少主键的表,InnoDB会内置一列用于聚簇索引来组织数据。而没有建立主键的话就没法通过主键来进行索引,查询的时候都是全表扫描,小数据量没问题,大数据量就会出现性能问题。
但是,问题真的只是查询影响吗?不是的,对于生成的ROW_ID,其自增的实现来源于一个全局的序列,而所以有ROW_ID的表共享该序列,这也意味着插入的时候生成需要共享一个序列,那么高并发插入的时候为了保持唯一性就避免不了锁的竞争,进而影响性能。
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
{
row_id_t id;
mutex_enter(&(dict_sys->mutex));
id = dict_sys->row_id;
if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
dict_hdr_flush_row_id();
}
dict_sys->row_id++;
mutex_exit(&(dict_sys->mutex));
return(id);
}
缺少主键或者非空索引存在问题
- 使用不了主键索引,查询会进行全表扫描
- 影响数据插入性能,插入数据需要生成ROW_ID,而生成的ROW_ID是全局共享的,并发会导致锁竞争,影响性能
为每个表设置主键
既然知道InnoDB对数据的存储和处理都是基于聚簇索引的,那么,在建表时候要注意主键的重要性,为每个表都设置一个主键,如果没有合适的字段来作为主键,可以设置一个业务无关的的代理主键,可以是自增ID,也可以是UUID(建议使用自增ID,性能较好)。
总结
在理解InnoDB的数据结构之后自然而然就会知道主键的重要性,在建表的时候也不会忘记设置主键,无论表设计有无合适的唯一字段,都需要设置一个主键,提高性能的同时也是一种好的习惯,对于后续的拓展以及表之间关联都有一定的拓展性。
参考文章
- http://todorex.com/2018/05/10/数据库/数据库范式/
- http://c.biancheng.net/view/8264.html
- https://www.cnblogs.com/CNty/p/10941398.html
- https://www.cnblogs.com/UniqueColor/p/7234340.html
- https://www.cnblogs.com/saoge/p/14431536.html