文章目录
一、范式
1.1 简介
- 概念
- 在关系型数据库中,关于数据表设计的基本原则、规则就称为范式。
- 可以理解为,一张数据表的设计结构需要满足的某种设计标准的
级别
。 - 要想设计一个结构合理的关系型数据库,必须满足一定的范式。
- 哪些
目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,又称完美范式)。
- 键和相关属性的概念
球员表(player)
:球员编号 | 姓名 | 身份证号 | 年龄 | 球队编号球队表(team)
:球队编号 | 主教练 | 球队所在地
键 / 属性 | 说明 |
---|---|
超键 | 对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号)(球员编号,姓名)(身份证号,年龄)等。 |
候选键 | 就是最小的超键,对于球员表来说,候选键就是(球员编号)或者(身份证号)。 |
主键 | 我们自己选定,也就是从候选键中选择一个,比如(球员编号)。 |
外键 | 球员表中的球队编号。 |
主属性 、 非主属性 | 在球员表中,主属性是(球员编号)(身份证号),其他的属性(姓名)(年龄)(球队编号)都是非主属性。 |
- 函数依赖关系
依赖 | 说明 / 举例 |
---|---|
函数依赖 | 设R(U)是属性集U上的关系模式,X,Y是U的子集。若对于R(U)的任意一个可能的关系r,r不可能存在两个元组在X上的属性值相等,而在Y上的属性值不等,则称X函数确定Y或Y函数依赖于X,记作X->Y。 |
一个身份证号码可以确定一个人的姓名,不存在说身份证号相同,但是是不同的人的情况。 | |
平凡依赖 | 若X->Y,且Y是X的子集(对任一关系模式,平凡函数依赖必然成立),就是平凡函数依赖。 |
在学生表(学号,姓名,年级)中,(学号,姓名)可以推出学号和姓名其中的任何一个,这就是平凡函数依赖。 | |
非平凡依赖 | 若X->Y,但Y不是X的子集,就是非平凡函数依赖。 |
在学生表(学号,姓名,年级)中,通过(学号,姓名)可以推出这个学生所在的年级,但年级不是(学号,姓名)的子集,这是非平凡函数依赖。 | |
部分函数依赖 | 设X,Y是关系R的两个属性集合,存在X→Y,若X’是X的真子集,存在X’→Y,则称Y部分函数依赖于X。 |
学生基本信息表R中(学号,身份证号,姓名),(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖与(学号,身份证号)。 | |
完全函数依赖 | 设X,Y是关系R的两个属性集合,X’是X的真子集,存在X→Y,但对每一个X’都有X’!→Y,则称Y完全函数依赖于X。 |
学生基本信息表R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在R关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级)。 | |
传递函数依赖 | 设X,Y,Z是关系R中互不相同的属性集合,存在X→Y(Y !→X),Y→Z,则称Z传递函数依赖于X。 |
在关系R(学号 ,宿舍, 费用)中,(学号)->(宿舍),宿舍!=学号,(宿舍)->(费用),费用!=宿舍,所以符合传递函数的要求。 | |
多值依赖 | 设R(U)是属性集U上的一个关系模式。X,Y,Z是U的子集,并且Z=U-X-Y。关系R(U)中多值依赖X→→Y成立,当且仅当对R(U)的任一关系r,给定的一对(x,z)值,有一组Y的值,这组值仅仅决定于x的值而与z值无关。 |
平凡的多值依赖 | 全集U=K+A,一个K可以对应于多个A,即K→→A。此时整个表就是一组一对多关系。 |
非平凡的多值依赖 | 全集U=K+A+B,一个K可以对应于多个A,也可以对应于多个B,A与B互相独立,即K→→A,K→→B。 |
关系模式RDP(R,D,P)中,R表示医院的病房,D表示护士,P表示病人。假设每个病房有若干护士看管,有若干病人,每个护士必须看管所在病房的所有病人,每个病人要求是要被所在病房的所有护士看管的。 | |
连接依赖 | 设关系模式R、Ri的属性集是U、Ui,UiU(1≤i≤n).若R每个容许的实例r均满足r=∏U1(r)∞...∞∏Un(r)则称R满足连接依赖,记作∞(R1,...,Rn).若其中某个Ui=U,则称连接依赖是平凡连接依赖。 多值依赖也是连接依赖。 |
1.2 第一范式(1st NF)
1NF是对属性的原子性约束,要求属性具有原子性,不可再分解。
-
改造前
-
改造后
1.3 第二范式(2nd NF)
在满足第一范式(1NF)的基础上,每一个非主属性必须完全依赖候选键。
- 改造前:主键(球员编号,比赛编号)
比赛表 player_game
里面包含球员编号、姓名、年龄、比赛编号、比赛时间、比赛场地和得分等属性;- (球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分);
- (球员编号) → (姓名,年龄);
- (比赛编号) → (比赛时间, 比赛场地);
- 非完全依赖候选键产生的问题
数据冗余
:如果一个球员可以参加 m 场比赛,那么球员的姓名和年龄就重复了 m-1 次。一个比赛也可能会有 n 个球员参加,比赛的时间和地点就重复了 n-1 次。插入异常
:如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。删除异常
:如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。更新异常
:如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况。
- 改造后
表名 | 属性(字段) |
---|---|
球员 player 表 | 球员编号*、姓名和年龄等属性 |
比赛表 game 表 | 比赛编号*、比赛时间和比赛场地等属性 |
球员比赛关系 player_game 表 | 球员编号*、比赛编号*和得分等属性 |
1.4 第三范式(3rd NF)
任何非主属性都直接依赖于主属性,不能传递依赖于主属性。
- 改造前
球员player表
:球员编号、姓名、球队名称和球队主教练。
- 球员编号决定了球队名称,同时球队名称决定了球队主教练,非主属性球队主教练就会传递依赖于球员编号。
- 改造后
表名 | 属性(字段) |
---|---|
球员表 | 球员编号、姓名和球队名称 |
球队表 | 球队名称、球队主教练 |
1.5 巴斯范式(BCNF)
任何非主属性不能对主键子集依赖,即在 3NF 的基础上消除了主属性对候选键的部分依赖或者传递依赖关系。
- 改造前:主键(StudentId,Major)
- 这个表的设计满足三范式,但是这里存在另一个依赖关系,
专业
依赖于导师
。 - (StudentId,Major)->(Advisor),(Major)->(Advisor),所以
Advisor
部分函数依赖于(StudentId,Major)。
- 改造后
1.6 第四范式
在满足巴斯-科德范式(BCNF)的基础上,消除非平凡且非函数依赖的多值依赖。
- 改造前
- 每门课程有对应的一组教师,每门课程也有对应的一组教材,一门课程使用的教材和教师没有关系。
课程ID,教师ID,教材ID
这三列作为联合主键,为了表述方便用 Name 代替 ID。
- 这个表除了主键,就没有其他字段了,所以肯定满足BC范式,但是却存在
多值依赖
,即教材与教师无关。
- 改造后
1.7 第五范式、域键范式
- 除了第四范式外,我们还有更高级的第五范式(又称完美范式)和域键范式(DKNF)。
- 在满足第四范式(4NF)的基础上,消除不是由候选键所蕴含的连接依赖。如果 关系模式R中的每一个连接依赖均由R的候选键所隐含,则称此关系模式符合第五范式。
- 函数依赖是多值依赖的一种特殊的情况,而多值依赖实际上是连接依赖的一种特殊情况。但连接依赖不像函数依赖和多值依赖可以由
语义直接导出
,而是在关系连接运算
时才反映出来。存在连接依赖的关系模式仍可能遇到数据冗余及插入、修改、删除异常等问题。 - 第五范式处理的是
无损连接问题
,这个范式基本没有实际意义
,因为无损连接很少出现,而且难以察觉。而域键范式试图定义一个终极范式
,该范式考虑所有的依赖和约束类型,但是实用价值也是最小的,只存在理论研究中。
- 改造前
- 有一个销售信息表SALES(SALEPERSON,VENDOR,PRODUCT)。
- SALEPERSON 代表销售人员,VENDOR 代表供和商,PRODUCT 则代表产品。
- 改造后
- PERSON_VENDOR 表(SALEPERSON,VENDOR)
- PERSON_PRODUCT 表(SALEPERSON,PRODUCT)
- VENDOR_PRODICT 表(VENDOR,PRODUCT)
二、ER 模型
- ER 模型中有三个要素,分别是实体、属性和关系。
- 实体 可以看做是数据对象,往往对应于现实生活中的真实存在的个体。在 ER 模型中,用
矩形
来表示。实体分为两类,分别是强实体
和弱实体
。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系的实体。 - 属性 则是指实体的特性。比如超市的地址、联系电话、员工数等。在 ER 模型中用
椭圆形
来表示。 - 关系 则是指实体之间的联系。比如超市把商品卖给顾客,就是一种超市与顾客之间的联系。在 ER 模型中用
菱形
来表示。
- 关系的类型
- 一对一 指实体之间的关系是一一对应的,比如个人与身份证信息之间的关系就是一对一的关系。一个人只能有一个身份证信息,一个身份证信息也只属于一个人。
- 一对多 指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关系,则只能对应唯一的一边的实体。比如说,我们新建一个班级表,而每个班级都有多个学生,每个学生则对应一个班级,班级对学生就是一对多的关系。
- 多对多 指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。再比如一个选课表,有许多科目,每个科目有很多学生选,而每个学生又可以选择多个科目,这就是多对多的关系。
2.1 建模分析
设计有一定规模的应用,在项目的初始阶段就应建立完整的 ER 模型,开发应用项目的实质就是建模。
- 模拟
电商业务
- 实体:地址实体、用户实体、购物车实体、评论实体、商品实体、商品分类实体、订单实体和订单详情实体。
- 其中,
用户
和商品分类
是强实体,因为它们不需要依赖其他任何实体。
- 8个实体之间的
8种关系
- 用户可以在电商平台添加多个地址
- 用户只能拥有一个购物车
- 用户可以生成多个订单
- 用户可以发表多条评论
- 一件商品可以有多条评论
- 每一个商品分类包含多种商品
- 一个订单可以包含多个商品,一个商品可以在多个订单里
- 订单中又包含多个订单详情,因为一个订单中可能包含不同种类的商品
2.2 模型细化
- 实体的属性
地址实体
包括用户编号、省、市、地区、收件人、联系电话、是否是默认地址。用户实体
包括用户编号、用户名称、昵称、用户密码、手机号、邮箱、头像、用户级别。购物车实体
包括购物车编号、用户编号、商品编号、商品数量、图片文件url。订单实体
包括订单编号、收货人、收件人电话、总金额、用户编号、付款方式、送货地址、下单时间。订单详情实体
包括订单详情编号、订单编号、商品名称、商品编号、商品数量。商品实体
包括商品编号、价格、商品名称、分类编号、是否销售,规格、颜色。评论实体
包括评论id、评论内容、评论时间、用户编号、商品编号。商品分类实体
包括类别编号、类别名称、父类别编号。
2.3 模型图转数据表
创建 ER 模型不是目的,目的是把业务逻辑梳理清楚,设计出优秀的数据库。
2.4 数据表的设计原则
- 数据表的个数越少越好
- 数据表中的字段个数越少越好
- 数据表中联合主键的字段个数越少越好
- 使用主键和外键越多越好
三、数据库对象编写建议
3.1 关于库
- 【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以英文字母开头。
- 【强制】库名中英文
一律小写
,不同单词采用下划线
分割。须见名知意。 - 【强制】库的名称格式:业务系统名称_子系统名。
- 【强制】库名禁止使用关键字(如type,order等)。
- 【强制】创建数据库时必须
显式指定字符集
,并且字符集只能是utf8或者utf8mb4。创建数据库SQL举例:CREATE DATABASE crm_fundDEFAULT CHARACTER SET 'utf8'
; - 【建议】对于程序连接数据库账号,遵循
权限最小原则
,使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号原则上不准有drop权限
。 - 【建议】临时库以
tmp_
为前缀,并以日期为后缀;备份库以bak_
为前缀,并以日期为后缀。
3.2 关于表、列
- 【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议以
英文字母开头
。 - 【强制】
表名、列名一律小写
,不同单词采用下划线分割。须见名知意。 - 【强制】表名要求有模块名强相关,同一模块的表名尽量使用
统一前缀
。比如:crm_fund_item - 【强制】创建表时必须
显式指定字符集
为utf8或utf8mb4。 - 【强制】表名、列名禁止使用关键字(如type,order等)。
- 【强制】创建表时必须
显式指定表存储引擎
类型。如无特殊需求,一律为InnoDB。 - 【强制】建表必须有comment。
- 【强制】字段命名应尽可能使用表达实际含义的英文单词或
缩写
。如:公司 ID,不要使用 corporation_id, 而用corp_id 即可。 - 【强制】布尔值类型的字段命名为
is_描述
。如member表上表示是否为enabled的会员的字段命名为 is_enabled。 - 【强制】禁止在数据库中存储图片、文件等大的二进制数据通常文件很大,短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时。通常存储于文件服务器,数据库只存储文件地址信息。
- 【建议】建表时关于主键:
表必须有主键
(1)强制要求主键为id,类型为int或bigint,且为 auto_increment 建议使用unsigned无符号型。 (2)标识表里每一行主体的字段不要设为主键,建议设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机插入,则会导致innodb内部页分裂和大量随机I/O,性能下降。 - 【建议】核心表(如用户表)必须有行数据的
创建时间字段
(create_time)和最后更新时间字段
(update_time),便于查问题。 - 【建议】表中所有字段尽量都是
NOT NULL
属性,业务可以根据需要定义DEFAULT值
。 因为使用NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题。 - 【建议】所有存储相同数据的
列名和列类型必须一致
(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 - 【建议】中间表(或临时表)用于保留中间结果集,名称以
tmp_
开头。备份表用于备份或抓取源表快照,名称以bak_
开头。中间表和备份表定期清理。 - 【示范】一个较为规范的建表语句:
CREATE TABLE user_info (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_id` bigint(11) NOT NULL COMMENT '用户id',
`username` varchar(45) NOT NULL COMMENT '真实姓名',
`email` varchar(30) NOT NULL COMMENT '用户邮箱',
`nickname` varchar(45) NOT NULL COMMENT '昵称',
`birthday` date NOT NULL COMMENT '生日',
`sex` tinyint(4) DEFAULT '0' COMMENT '性别',
`short_introduce` varchar(150) DEFAULT NULL COMMENT '一句话介绍自己,最多50个汉字',
`user_resume` varchar(300) NOT NULL COMMENT '用户提交的简历存放地址',
`user_register_ip` int NOT NULL COMMENT '用户注册时的源ip',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '修改时间',
`user_review_status` tinyint NOT NULL COMMENT '用户资料审核状态,1为通过,2为审核中,3为未通过,4为还未提交审核',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_user_id` (`user_id`),
KEY `idx_username`(`username`),
KEY `idx_create_time_status`(`create_time`,`user_review_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='网站用户基本信息';
- 【建议】创建表时,可以使用可视化工具。这样可以确保表、字段相关的约定都能设置上。实际上,我们通常很少自己写 DDL 语句,可以使用一些可视化工具来创建和操作数据库和数据表。可视化工具除了方便,还能直接帮我们将数据库的结构定义转化成 SQL 语言,方便数据库和数据表结构的导出和导入。
3.3 关于索引
- 【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值
禁止被更新
。 - 【强制】InnoDB和MyISAM存储引擎表,索引类型必须为
BTREE
。 - 【建议】主键的名称以
pk_
开头,唯一键以uni_
或uk_
开头,普通索引以idx_
开头,一律使用小写格式,以字段的名称或缩写作为后缀。 - 【建议】多单词组成的columnname,取前几个单词首字母,加末单词组成column_name。如:sample 表 member_id 上的索引:idx_sample_mid。
- 【建议】单个表上的索引个数
不能超过6个
。 - 【建议】在建立索引时,多考虑建立
联合索引
,并把区分度最高的字段放在最前面。 - 【建议】在多表 JOIN 的SQL里,保证被驱动表的连接列上有索引,这样JOIN 执行效率最高。
- 【建议】建表或加索引时,保证表里互相不存在
冗余索引
。 比如:如果表里已经存在key(a,b),则key(a)为冗余索引,需要删除。
3.4 SQL 编写
- 【强制】程序端SELECT语句必须指定具体字段名称,禁止写成 *。
- 【建议】程序端insert语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)。
- 【建议】除静态表或小表(100行以内),DML语句必须有WHERE条件,且使用索引查找。
- 【建议】INSERT INTO…VALUES(XX),(XX),(XX)… 这里XX的值不要超过5000个。 值过多虽然上线很快,但会引起主从同步延迟。
- 【建议】SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。
- 【建议】线上环境,多表 JOIN 不要超过5个表。
- 【建议】减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDER BY、GROUP BY、DISTINCT 这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
- 【建议】包含了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;否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。