插曲
最近,一个远房亲戚的小表弟准备选修专业
找到我问:
"哥,现在学数据库有没有前途阿?"
"当然有啊,前途大大的呢"
"那我现在开始学数据库,需要先从什么开始呢?"
"学课程的话,先了解下数据库三范式,SQL这些吧"
"SQL我大概知道,数据库三范式是什么?"
"阿...三范式就是表的主键...唯一性那些东西吧,...嗯,应该就是那些"
"什么是主键?"
"额.....表弟你不要再问了啦,好好去百度一下行不。"
"噢...."
挂完电话,我舒了口气,由于差点暴露自己已经不记得三范式了这个不争的事实,我悄悄打开了谷歌....
数据库的这个三范式的概念,相信大多数人都不会陌生,从懵懵懂懂的大学时代就已经普及到教材了(没记错的话应该在数据库系统概论这本教材里)。
还记得那会刚开始找实习的时候,由于自己本事太小,连简历都不知道怎么写好,尤其是擅长技术的部分更是一片空白。
于是乎会找来隔壁几个学霸的简历来做参考,那会儿大家的简历上都会赫赫写着:
熟练掌握数据库三范式,精通数据库系统开发语言。
又或者是:
熟悉ER图制作工具,能实现满足三范式的数据库设计
一开始觉得数据库三范式确实是个好东西,以至于面试的时候技术官没有提问到三范式的细节,自己感到了惊讶和茫然。
随着工作经验逐渐见长,数据库范式理论在脑海里的强印象渐渐消除。我在想,要么是记忆的衰退,要么就是有些原则已经形成了本能的经验了。
那么,什么是数据库的范式?
三范式的定义
这里,不想花太多的篇幅去讨论理论性的东西,这些信息一抓一大把。我们就通过一些简单的例子来体会一下。
1. 第一范式
假设有一张用户信息表,上面除了用户编号、姓名之外,还会记录地址信息:
编号 | 姓名 | 性别 | 所在地 |
---|---|---|---|
0001 | 张三 | 男 | 广东省,深圳市 |
0002 | 李四 | 女 | 海南省,海口市 |
在这里面,地址信息一栏就是不符合第一范式(1NF)的:
第一范式(1NF):数据库表的每一列都是不可分割的原子项
因此,应该拆分为:
编号 | 姓名 | 性别 | 所在省 | 地市 |
---|---|---|---|---|
0001 | 张三 | 男 | 广东省 | 深圳市 |
0002 | 李四 | 女 | 海南省 | 海口市 |
2. 第二范式
以一个订单表为例,通常在淘宝上下单时会产生包含多个商品的订单,如下:
订单号 | 商品号 | 商品名称 | 价格 |
---|---|---|---|
o1 | g1 | 洗衣液 | 23 |
o1 | g2 | 吹风机 | 125 |
o1 | g3 | 蚕豆 | 5 |
o2 | g9 | 被子 | 302 |
o2 | g8 | 枕头 | 69 |
这里同样违反了第二范式的定义:
第二范式(2NF):每个表必须有且仅有一个数据元素为主键(Primary key),其他属性需完全依赖于主键
第二范式需建立在满足第一范式的基础之上
第二范式首先要求的是存在一个唯一的主键,在上面的表中,就必须将 订单号、商品号 作为一个联合的主键才能满足要求。
那么对于第二点要求呢? 其他属性是否依赖于这个主键?
在订单的场景中,我们可以认为这算是合理的,因为商品的价格甚至名称都可能会发生变化,而在每个订单中所看到的这些信息都应该是不变的,
谁也不希望看到自己已经支付的订单中的商品信息突然大降价.. 当然更重要的还是保持订单总价与商品单价记录的一致性。
因此这里的记录可以认为是商品信息在创建订单时的一个快照。
但是,对于下面的这一场景可能就不合适了:
订单号 | 商品号 | 商品名称 | 价格 | 商品类别 |
---|---|---|---|---|
o1 | g1 | 洗衣液 | 23 | 家居 |
o1 | g2 | 吹风机 | 125 | 电器 |
o1 | g3 | 蚕豆 | 5 | 食品 |
o2 | g9 | 被子 | 302 | 家居 |
o2 | g8 | 枕头 | 69 | 家居 |
商品所属的类别一般是固定的,也就是商品的类别属性仅仅与商品编号相关,即仅仅是依赖于主键的一部分。
这就违反了第二范式中"其他属性必须完全依赖于主键"的规则,因此需要将该属性分离到商品信息表中。
3. 第三范式
让我们回到一开始的用户表,如果在用户信息表中,同时补充一些城市的信息:
编号 | 姓名 | 性别 | 城市 | 城市特色 | 城市人口 |
---|---|---|---|---|---|
0001 | 张三 | 男 | 深圳市 | 科技、创新 | 1300W |
0002 | 李四 | 女 | 海口市 | 旅游、观光 | 230W |
这样便违反了第三范式的定义:
第三范式(3NF):数据表中的每一列都和主键直接相关,而不能间接相关
同样,第三范式也需要建立在第二范式的基础之上
很明显,这里的城市人口、特色等属性都仅仅依赖于用户所在的城市,而不是用户,只能算间接的关系。
因此最好的做法是将城市相关的属性分离到一个城市信息表中。
为什么需要范式
数据库范式为数据库的设计、开发提供了一个可参考的典范,在许多教学材料中也是作为关键的课程内容。
那么范式的提出是为了解决什么问题?
第一范式,要求将列尽可能最小的分割,希望消除某个列存储多个值的冗余的行为
比如用户表中的地址信息,拆分为省、市这种明确的字段,可以按独立的字段检索、查询第二范式,要求唯一的主键,且不存在对主键的部分依赖,希望消除表中存在冗余(多余)的列
比如订单表中的商品分类、详情信息,只需要由商品信息表存储一份即可。第三范式,要求没有间接依赖于主键的列,即仍然是希望消除表中冗余的列
比如用户表中不需要存储额外的 其所在城市的人口、城市特点等信息。
很明显,这些范式大都是为了消除冗余而提出的,这有利于数据的一致性,当然也可以尽可能的减少存储成本。
PS:你懂得三范式,可以帮老板省钱,难怪简历上要写上..
除了本文中提到的三范式之外,实质上还有BCNF范式、第四、第五范式。
借助三范式的理念,你可以设计出很精炼的数据库表结构。然而现有的项目应用并不会完全遵循范式的理念,原因比如:
性能原因,没有任何冗余的表设计会产生更多的查询行为,这意味着会产生更多次的数据库IO操作。在一些实时交互的系统中,可能会慢得让人难以忍受。
当然,你可以使用数据库的 连接(join) 操作,而事实上数据库提供 join 也就是为了来缓解这种问题。但一旦用到了分库分表方案,这个问题就会非常的棘手。成本结构的变化,数据库范式是在20世纪提出的,当时的磁盘存储成本还很高。随着科技发展,数据存储的成本已经大幅度缩减,对于采用范式设计(规避冗余)带来的成本缩减收益已经不那么明显。
See the source image
反范式设计
既然范式是为了消除冗余,那么反范式就是通过增加冗余、聚合的手段来提升性能。比如,为了提升查询的性能,在CMS的文章表中同时冗余作者的信息。
冗余的做法会牺牲一定的数据一致性,通常也是需要业务上进行权衡取舍。
当然,除了冗余(存储多份拷贝) 之外,还有另外的理念,即数据的聚合,或者叫嵌套。这种做法相当于是将多个字段(列)合并存储到数据库表的一个列中。
比如一条订单数据就可以同时包含许多信息:
{
"oid": "0001",
"price": {
"total": 380,
"benefit": 40
},
"goods": [{
"gid": "SN001",
"name": "蓝月亮洗衣液",
"price": 41,
"amount": 2
},
{
"gid": "SN003",
"name": "电动剃须刀",
"price": 99,
"amount": 1
}
],
"address": {
"contact": "张三",
"phone": "150899000"
...
}
...
}
这种灵活的结构几乎是 NoSQL的专利,比如MongoDB文档数据库就可以直接以内嵌数组、对象的形式来实现聚合式存储,这无疑带来了极大的灵活性。
而 MySQL 在5.7.2版本开始支持JSON结构化列,也进入了聚合式存储的队伍,与其对标的PostGreSQL 则是9.4版本就已经支持。
反范式的做法在互联网项目、开源产品中也比较常见,比如Discuz 的数据表设计中就存在许多的冗余列、聚合字段。
一方面,除了能获得性能的提升之外,数据压缩、高度灵活扩展(非结构化) 也是反范式设计能获得青睐的理由。
当然,这里并非一律反对数据库范式,理解范式仍然是做好数据库设计的一门基础,比如选择合适的主键、清晰的划分每一列属性等等。
在项目中仍然需要根据自身的业务特点在范式和反范式中找到平衡点(通常是两者的结合)。类似于架构设计中空间换时间的一些做法,这其中涉及到的各种取舍都是需要经过权衡的。
也可以说这是一门“艺术“,因为没有标准答案...