当数据库运行一段时间之后,我们才发现数据表设计的有问题。重新调整数据表的结构,就需要做数据迁移,还有可能影响程序的业务逻辑,以及网站正常的访问。
如果是糟糕的数据库设计可能会造成一下问题:
良好的数据设计原则有以下优点:
总之,开始设置数据库的时候,我们就需要重视数据表的设计。为了建立冗余较小、结果合理的数据库,设计数据库时必须遵循一定的规则。
范式简介
在关系型数据库中,关于数据表的设计的基本原则、规则就称为范式。可以理解为,一张数据表的设计结构需要满足的某种设计标准的级别。要想设计一个结构合理的关系型数据库,必须满足一定的范式。
范式的英文名称是Normal Form
,简称NF
。它是英国人E.F.Codd
在上个世纪70年代提出的关系型数据库模型后总结出来的。范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。
范式都包括哪些
目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:第一范式(1NF)
、第二范式(2NF)
、第三范式(3NF)
、巴斯-科德范式(BCNF
)、第四范式(4NF)
和第五范式(5NF
,又称完美范式)。
数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,满足最低要求的范式是第一范式(1NF)
。在第一范式的基础上进一步满足更多规范要求的称为第二范式(2NF)
。其余范式以此类推。
一般来说,在关系型数据库设计中,最高也就遵循到BCNF
、普遍还是3NF
。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反规范化。
键和相关属性的概念
范式的定义会使用到主键和候选键,数据库中的键(Key
)由一个或者多个属性组成。数据表中常用的几种键和属性的定义:
R1
中的某属性不是R1
的主键,而是另一个数据表R2
的主键,那么这个属性集就是数据表R1
的外键。通常:我们也将候选键称之为码,把主键也称为主码。因为键可能是由多个属性组成的,针对单个属性,我们还可以用主属性和非主属性来进行区分。
第一范式(1 st NF
)
第一范式主要是确保数据表中每个字段的值必须具有原子性,也就是说数据表中每个字段的值为不可再次拆分的最小数据单元。
举例:
user
表的设计不符合第一范式:
字段名称 | 字段类型 | 是否是主键 | 说明 |
---|---|---|---|
id |
INT |
是 | 主键Id |
username |
VARCHAR(30) |
否 | 用户名 |
password |
VARCHAR(50) |
否 | 密码 |
user_info |
VARCHAR(255) |
否 | 用户信息(包含真实姓名、电话、住址) |
其中:user_info
字段为用户信息,可以进一步拆分成更小粒度的字段,不符合数据库对第一范式的要求。将user_info
拆分后如下:
字段名称 | 字段类型 | 是否是主键 | 说明 |
---|---|---|---|
id |
INT |
是 | 主键id |
username |
VARCHAR(30) |
否 | 用户名 |
password |
VARCHAR(50) |
否 | 密码 |
real_name |
VARCHAR(30) |
否 | 真实姓名 |
phone |
VARCHAR(12) |
否 | 联系电话 |
address |
VARCHAR(100) |
否 | 家庭住址 |
第二范式(2 st NF
)
第二范式要求,在满足第一范式的基础上,还要满足数据表里有的每一条数据记录,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。如果知道主键的所有属性的值,就可以检索到任何元组(行)的任何属性的任何值。(要求中的主键,其实可以拓展替换为候选键。)
举例:
比赛表player_game
,里面包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地等属性,这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键(或主键)来决定如下的关系:
(球员编号, 比赛编号) ——> (姓名, 年龄, 比赛时间, 比赛场地, 得分)
但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:
(球员编号) ——> (姓名, 年龄)
(比赛编号) ——> (比赛时间, 比赛场地)
对于非主属性来说,并非完全依赖候选键。这样会产生怎样的问题呢?
m
场比赛,那么球员的姓名和年龄就重复了m - 1
次。一个比赛也可能会有n
个球员参加,比赛的时间和地点就重复了n - 1
次。为了避免出现上述的情况,我们可以把球员比赛表设计为下面的三张表:
表名 | 属性(字段) |
---|---|
球员player 表 |
球员编号、姓名和年龄等属性 |
比赛game 表 |
比赛编号、比赛时间和比赛场地等属性 |
球员比赛关系player_game 表 |
球员编号、比赛编号和得分等属性 |
第三范式(3 st NF
)
第三范式是在第二范式的基础上,确保数据表中的每一个非主键字段都和主键字段直接相关,也就是说,要求数据表中的所有主键字段不能依赖于其他非主键字段。通俗的讲,该规则的意思是所有非主键属性之间不能有依赖关系,必须相互独立。
举例
字段名称 | 字段类型 | 是否是主键 | 说明 |
---|---|---|---|
id |
INT |
是 | 商品主键id (主键) |
category_id |
INT |
否 | 商品类别id |
category_name |
VARCHAR(30) |
否 | 商品类型名称 |
goods_name |
VARCHAR(30) |
否 | 商品名称 |
price |
DECIMAL(10, 2) |
否 | 商品价格 |
商品类别名称依赖于商品类别编号,不符合第三范式。修改
表一:符合第三范式的商品类别表的设计
字段名称 | 字段类型 | 是否是主键 | 说明 |
---|---|---|---|
id |
INT |
是 | 商品类别主键id |
category_name |
VARCHAR(30) |
否 | 商品类别名称 |
表二:符合第三范式的商品表的设计
字段名称 | 字段类型 | 是否是主键 | 说明 |
---|---|---|---|
id |
INT |
是 | 商品主键id |
category_id |
VARCHAR(30) |
否 | 商品类型id |
goods_name |
VARCHAR(30) |
否 | 商品名称 |
price |
DECIMAL(10, 2) |
否 | 商品价格 |
商品表goods
通过商品类别id
字段(category_id
)与商品类别表goods_category
进行关联。
范式的优缺点
优点
数据的标准化有助于消除数据库中的数据冗余,第三范式(3NF
)通常被认为在性能、扩展性和数据完整性方面达到了最好的平衡
缺点
范式的使用,可能降低查询的效率。因为范式等级越高,设计出来的数据表就越多、越精细,数据的冗余度就越低,进行数据查询的时候就可以需要关联多张表,但不代价昂贵,也可能使一些索引策略无效。
范式只是提出了设计的标准,实际上设计数据表时,未必一定要符合这些标准。开发中,我们会出现为了性能和读取效率违反范式化的原则,通过增加少量的冗余或重复的数据来提高数据库的读性能,减少关联查询,join
表的次数,实现空间换取时间的目的。因此在实际的设计过程中要理论结合实际,灵活运用。
概述
有时候不能简单按照规范要求设计数据表,因为有的数据看似冗余,其实对业务来说十分重要。这个时候,我们就要遵循业务优先的原则,首先满足业务需求,在尽量减少冗余。
如果数据库中的数据量比较大,系统的UV
和PV
访问频次比较高,则完全按照MySQL
的三大范式设计数据表,读数据时会产生大量的关联查询,在一定程度上会影响数据库的读性能。如果我们想对查询效率进行优化,反范式化也是一种优化思路。此时,可以通过在数据表中增加冗余字段来提高数据库的读性能。
VS
性能
应用举例
我们有2
个表,分别是商品流水表(trans)
和商品信息表(goodsinfo)
。商品流水表里有400万条流水记录,商品信息表里有2000条商品记录。
两个表是符合第三范式要求的。但是,在我们项目的实施过程中,对流水的查询频率很高,而且为了获取商品名称,基本都会用到与商品信息表的连接查询。
为了减少连接,我们可以直接把商品名称字段加到流水表里面。这样一来,我们就可以直接从流水表中获取商品名称字段了。虽然增加了冗余字段,但是避免了关联查询,提升了查询的效率。
反范式的新问题
反范式话可以通过空间换时间,提升查询的效率,但是反范式也会带来一些新问题。
反范式的适用场景
当冗余信息有价值或者能大幅度提高查询效率的时候,我们才会采取反范式的优化。
增加冗余字段的建议
增加冗余一定要符合如下两个条件。只有满足这两个条件,才可以考虑增加冗余字段。
历史快照、历史数据的需要
在现实生活中,我们经常需要一些冗余信息,每次发生的订单收货信息都属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。
反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析。
简单总结下数据仓库和数据库在使用上的区别:
概述
人们在3NF
的基础上进行了改进,提出了巴斯范式(BCNF
),也叫作巴斯-科德范式(Boyce-Codd Normal Form
)。BCNF
被认为没有新的的设计规范加入,只是对第三范式中设计规范要求更强,使得数据冗余度更小。所以,称为是修正的第三范式,或扩充的第三范式,BCNF
不被称为第四范式。
若一个关系达到了第三范式,并且它只有一个候选键,或者它的每个候选键都是单属性,则该关系自然达到BC
范式。
应用举例
有一个学生导师表,其中包含字段:学生ID
、专业、导师,专业GPA
,这其中学生ID
和专业是联合主键。
StudentID |
Major |
Advisor |
MajGPA |
---|---|---|---|
1 | 人工智能 | Edward |
4.0 |
2 | 大数据 | William |
3.8 |
1 | 大数据 | William |
3.7 |
3 | 大数据 | Joseph |
4.0 |
这个表的设计满足三范式,但是这里存在另一个依赖关系,专业依赖于导师,也就是说每个导师只做一个专业方面的导师,只要知道了是哪个导师,我们自然就知道是哪个专业的了。
所以这个表的部分主键Major
依赖于非主键属性的Advisor
,那么我们可以进行调整,拆分成2个表:学生导师表、导师表
StudentID |
Advisor |
MajGPA |
---|---|---|
1 | Edward |
4.0 |
2 | William |
3.8 |
1 | William |
3.7 |
3 | Joseph |
4.0 |
Advisor |
Major |
---|---|
Edward |
人工智能 |
William |
大数据 |
William |
大数据 |
Joseph |
大数据 |
多值依赖的概念
K ————> A
。U = K + A
,一个K
可以对应于多个A
,即K ————> A
。此时整个表就是一组一对多关系。U = K + A + B
,一个K
可以对应于多个A
,也可以对应于多个B
,A
与B
互相独立,即K ————> A
,K ———— B
。整个表由多组一对多关系,且有:-
部分是相同的属性集合,多部分是互相独立的属性集合。第四范式
第四范式即在满足巴斯-科德范式BCNF
的基础上,消除非平凡且非函数的多值依赖(即把同一表内的多对多关系删除)。
应用举例
职工表:职工编号、职工孩子姓名,职工选修课程。
在这个表中,同一个职工可能会有多个孩子姓名。同样,同一个职工也可能会有多个职工选修课,即这里存在着多值事实,不符合第四范式。
如果要符合第四范式,只需要将上表分为两个表,使他们只有一个多值事实,例如:职工表一(职工编号、职工孩子姓名)。职工表二(职工编号、职工选修课程)。两个表都只有一个多值事实,所以符合第四范式。
除了第四范式外,我们还有更高级的第五范式(又称完美范式)和域键范式(DKNF
)。
在满足第四范式(4NF
)的基础上,消除不是由候选键所蕴含的连接依赖。如果关系模式R
中的每一个连接依赖均由R
的候选键所隐含,则称此关系模式复合第五范式。
函数依赖是多值依赖的一种特殊的情况,而多值依赖实际上是连接依赖的一种特殊情况。但连接依赖不像函数依赖和多值依赖可以由语义直接导出,而是在关系连接运算时才反映出来。存在连接依赖的关系模式仍可能遇到数据冗余及插入、修改、删除异常等问题。
第五范式处理的是无损连接问题,这个范式基本没有实际意义,因为无损连接很少出现,而且难以察觉。而域键范式视图定义一个终极范式,该范式考虑所有的依赖和约束类型,但是实用价值也是最小的,只存在理论研究中。
数据表设计的一般原则:三少一多。
数据表的个数越少越好
RDBMS
的核心在于对实体和联系的定义,也就是E-R
图(Entity Relationship Diagram
),数据表越少,证明实体和联系设计得越简洁,既方便理解有方便操作。
数据表中的字段个数越少越好
字段个数越多,数据冗余的可能性越大。设置字段个数少的前提是各个字段相互独立,而不是某个字段的取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常会在数据冗余和检索效率中进行平衡。
数据表中联合主键的字段个数越少越好
设置主键是为了确定唯一性,当一个字段无法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键中的字段越多,占用的索引空间越大,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合主键的字段个数越少越好。
使用主键和外键越多越好
数据的设计设计上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率。
三少一多原则的核心就是简单可复用。简单指的是用更少的表、更少的字段、更少的联合主键字段来完成数据表的设计。可复用则是通过主键、外键的使用来增强数据表之间的复用率。
type
,order
等)。utf8
或者utf8mb4
。tmp_
为前缀,并以日期为后缀;备份库以 bak_
为前缀,并以日期为后缀。crm_fund_item
。utf8
或utf8mb4
。type
,order
等)。InnoDB
。comment
。ID
,不要使用corporation_id
, 而用corp_id
即可。is_描述
。如member
表上表示是否为enabled
的会员的字段命名为is_enabled
。id
,类型为int
或bigint
,且为auto_increment
建议使用unsigned
无符号型。user_id
,order_id
等,并建立unique key
索引。create_time
)和 最后更新时间字段(update_time)
,便于查问题。NOT NULL
属性,业务可以根据需要定义DEFAULT
值 。tmp_
开头。备份表用于备份或抓取源表快照,名称以bak_
开头。中间表和备份表定期清理。SQL
语言,方便数据库和数据表结构的导出和导入。InnoDB
表必须主键为id int/bigint auto_increment
,且主键值禁止被更新。InnoDB
和MyISAM
存储引擎表,索引类型必须为BTREE
。pk_
开头,唯一键以uni_
或uk_
开头,普通索引以idx_
开头,一律使用小写格式,以字段的名称或缩写作为后缀。columnname
,取前几个单词首字母,加末单词组成column_name
。JOIN
的SQL
里,保证被驱动表的连接列上有索引,这样JOIN
执行效率最高。SQL
编写
SELECT
语句必须指定具体字段名称,禁止写成*
。insert
语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)
。DML
语句必须有WHERE
条件,且使用索引查找。INSERT INTO…VALUES(XX),(XX)..
这里XX的值不要超过5000个。 值过多虽然上线很快,但会引起主从同步延迟。SELECT
语句不要使用UNION
,推荐使用UNION ALL
,并且UNION
子句个数限制在5个以内。JOIN
不要超过5个表。ORDER BY
,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDER BY
、GROUP BY
、DISTINCT
这些查询的语句,WHERE
条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。alter
操作必须合并为一次对于超过100W
行的大表进行alter table
,必须经过DBA
审核,并在业务低峰期执行,多个alter
需整合在一起。 因为alter table
会产生 表锁 ,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。sleep
。SQL
不超过5个。因为过长的事务会导致锁数据较久,MySQL
内部缓存、连接消耗过多等问题。UNIQUE KEY
,如UPDATE… WHERE id=XX
;否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。