数据库设计与建模最佳实践(转)
关键词:数据库对象命名,主键的设计,字段类型及长度设计,最佳实践范例
1. 引言
在企业级应用中,数据库是重要的组成部分。同样,数据库设计也是设计工作的重中之重。好的数据库设计可以简化开发、降低维护成本,更可以提高系统性能。
在这篇文章里,我针对可读性、易维护性及性能等方面,总结出以下数据库设计和建模最佳实践。
2. 数据库对象命名
为对象命名并非一件轻松随意的工作,下面的指导有利于构建一个规范的命名体系。
2.1 中文拼音还是英文单词
中文拼音gzzbh是什么意思呢?如果改成worker_code即可一目了然。因为英文单词语意比较精确,可读性比中文拼音好,所以推荐用英文单词给对象命名。
2.2 单词间分隔
用下划线分隔多个单词。
由于Oracle等数据库的对象名称默认不区分大小写(可以用双引号来区分,但是会给开发造成不必要的麻烦),所以在编程开发中常用的驼式命名风格如:tableName不太适合给数据库对象命名,因为对象的名称是以TABLENAME形式存储的。为了增强可读性,单词之间最好用下划线分隔,如:tableName 最好书写为 table_name。
2.3 避免使用关键字
用关键字命名对象会造成冲突。
如果一定用关键字命名对象,需要用双引号,如:id 命名为 "id"。
避免关键字冲突的较好方案是增加前缀或者后缀, 这也是所有对象命名的推荐方案,如:“用户编号”由"id" 改为更明确的user_id。
2.4 单数还是复数
用user还是users?
用child还是children?
英文名词有单数和复数之分,那对象命名时怎么考虑呢?
几乎所有的表都存储1条以上的记录,按这个事实,表名称应该全用复数才对,考虑到英文名词的复数变化比较复杂, 全用复数会加大设计者的负担,并且容易出错,从简化工作考虑,对象命名统一用单数即可。
2.5 用简写还是全称
dept_id, u_id分别代表什么意思呢?
对dept这个简写,一般情况下它是department的简写,而u_id的含义就不明确了。如果设计者把u_id改为user_id意义就明确了。
所以,除非是众所周知的简称,一般情况下最好用更明确的全称,不要让别人猜测命名的含义,因为可读性对系统的可维护性影响很大。
2.6 布尔型字段命名
有些字段的含义是代表布尔变量的:是、否;有、无。
为了强调这些字段的含义,可以在命名上增加 is_、has_、was_等前缀来增强可读性,如:
enable用is_enable;
updatable用is_updatable;
2.7 主键字段命名
主键名称推荐用:表名 + "_id" , 这样能够保证主键命名的唯一性和可读性,如:
user表的主键用user_id;
department表的主键用dept_id;
2.8 外键字段命名
外键字段名称最好与主表主键名称一致。
这样做可读性好, 而且建模工具能够根据主键、外键名称相同很方便的建立外键关联。
以上总结了数据库对象命名的基本原则,在此基础上,我们可以制定更详细的数据库命名规范。
3. 主键的设计
主键的设计作为表结构设计的重要组成部分,对开发和维护工作的影响不容忽视,值得我们仔细推敲。
3.1 主键的必要性
表一定要有主键吗?答案是肯定的。
表缺少了主键,表结构不再完整。主键是我们唯一标识一条记录的信息,在删除、修改记录时大都是通过主键来操作,没有了主键,这些操作会很困难。
3.2 主键业务无关性
早期的数据库设计,大多都是用业务信息做主键,在实践中吃了很多亏后,大家更认同用业务无关的字段来做主键。
比如用订单编号来做主键,开始大家觉得订单编号应该是唯一的编号。随着业务的变化,客户在使用中提出,订单可以作废,但是该记录不能删除,而且需要新建一个具有相同订单编号的订单,麻烦来了。
这是用业务相关信息做主键的典型缺陷,用业务无关性的信息做主键就不会有这样的问题。
3.3 单主键还是多主键
多主键其实是业务信息做主键时期的产物,既然我们确定了主键的业务无关性,利用单主键已经-能够唯一标识一条记录,所以多主键已?-没有继续使用的意义。
另外多主键还对开发造成很多麻烦,比如用Hibernate时,多主键需要单独创建一个主键对象。
单主键方案不仅仅是说主表用单主键,子表也要用单主键,多对多的中间表也要用单主键。
3.4 主键生成方案
在确定单主键的方案后,我们如何产生一个业务无关的键值呢?
有以下几种方案:
3.4.1 数值自增
数值自增是利用数据库自身的特性,如:Oracle里的序列来保证唯一性。
这种方案依赖具体的数据库产品,有移植的问题。该方案最大的隐患是数据集的合并问题,如果多个数据库的数据需要合并,由于编号重复,会给合并工作带来极大的困难。
3.4.2 最大值加1
每次从业务表里获得最大的编号,然后加1作为下一条记录的主键。这种方案只是产生编号的时期、方式与数值自增不同而已。
这种方案同样有数据合并困难的隐患,而且由于每次都要计算最大值,这种方案还有性能和并发的问题。
3.4.3 自制加1
这种方案是最大值加1方案的改进,它建立一个专门的表来维护最大值,从而提高了性能。
这种方案同样有数据合并困难的隐患,并且有并发问题。
3.4.4 全局唯一标识符GUID
GUID是由32位字符组成的全球唯一标识号,由特殊的算法来保证其计算结果的唯一性。GUID计算不依赖具体的数据库产品,也没有并发的问题。因为GUID值是全球唯一的,所以也不存在数据合并时主键冲突的问题。
这种方案的缺点是: 占用存储空间,可读性、表意性差。
GUID值是32位字母与数字的无序组合,很难记忆。如果用做数据字典主键,则很难看出具体的业务内容。
以上是产生业务无关主键的几种方案,通过对比不难得出,GUID做主键的方案最具有优势。
4.字段类型及长度
字段的类型和长度与具体业务有关,而业务需求又常常变化,所以我们要设计一个健壮的结构来尽量避免需求变化对数据库结构的影响。因为不同的数据库字段类型也不同,这里我们讨论的类型将以Oracle 8i为基准。
4.1 主键
前面提到主键采用GUID方案,所以主键至少要32位宽,有的GUID值中间有4个“-”字符,宽度是36位,向上取整,主键类型可以为40个字符。在Oracle 8i、9i数据库里char(40)会用空格补位,Oracle 10G里char已?-不会补空格了,但考虑到兼容性,选取varchar2(40)。
4.2 布尔类型
对于布尔类型的字段建议number(1) 而不是char(1)。
char(1)里可以存储0、1;Y/N;y/n等,由于值的不确定,特别是大小写问题,会给开发带来不必要的麻烦。
number(1)只能存0、1,意义很明显;
4.3 一般文字类型
不要过分相信用户需求,要尽量设计的宽松些,如:
车号:开发时是6位的,后来改为7位;
轴号:一般是8位,但是进口轴承有15位的;
轴承编号:一般是10位,后来又提出有20位的。
在用PowerBuilder开发系统时,数据窗口里会保存字段长度信息,如果改了数据库字段的长度,数据窗口也必须刷新才行,需要重新编译、发布系统。所以从系统易维护性考虑,一般文字字段类型可以长一些,可以与主键类型一致,都为40位字符宽,但是也不能太长,因为检索数据时会消耗内存。
4.4 数值类型
一般直接用number ,不指定精度和小数位是个较好的选择。
不指定精度的最大的好处是数值的精度和范围是可以变化的。如果根据业务需求,字段类型设计为number(2,1),如果用户在使用中提出小数点后要保留2位,则需要修改数据库和程序。
在确定没有小数位的情况下,最好指定精度,限制录入内容必须为整型数值。
比如布尔类型的字段, 最好为 number(1)
用于排序的字段, 可以为 number(30)
用于Hibernate乐观锁定的version字段, 可以为 number(30)
4.5 备注和长文本
这2者的长度根据具体情况来定。
备注的长度一般在100 – 500 之间。
长文本的长度在1000 – 4000之间。
4.6 日期型
没有什么特殊说明。
Oracle的日期类型不区分日期和时间类型。
4.7 LOB型
不要把lob类型字段放到业务表里,最好建立单独的表存储lob信息,这样有利于系统的性能和数据库的维护。
5.字段约束与默认值
数据库大都支持约束控制,从数据完整性控制和性能来说,这个工作最好交给数据库去做,尽量不要自己写代码。有些情况下,比如用户名重复的提示,可以在保存前通过代码先校验一下,从而给出更友好的提示,如:该用户已经注册。
字段的默认值和非空限制最好设置上,否则保存的有可能是空值,这样在开发时,对检索出来的值每次都要先判断空值,造成一点小麻烦。
6. 最佳实践范例
反面示例 | 最佳实践 | 改进说明 |
create table parent ( pid number, pname varchar2(40), cdate date, enable char(1), uid varchar2(40) ); | create table mf_db_parent ( parent_id varchar2(40), parent_name varchar2(40) not null, create_date date default? sysdate not null , is_enable number(1) default? 1 not null , create_user_id varchar2(40) ); | 用意义明确的全称 |
alter table parent add constraint pk_parent primary key(pid); | alter table mf_db_parent add constraint pk_mf_db_parent primary key(parent_id); | 无 |
create table child( cid number(40), cname varchar2(40), pid number(40) ); | create table mf_db_child( child_id varchar2(40), child_name varchar2(40) not null, parent_id varchar2(40) ); | 用意义明确的全称 |
alter table child add constraint pk_child primary key(cid); | alter table mf_db_child add constraint pk_mf_db_child primary key(child_id); | 无 |
alter table child add constraint fk_child_r_parent foreign key(pid) references t_parent (pid) on delete cascade; | alter table mf_db_child add constraint fk_child_r_parent foreign key(parent_id) references mf_db_parent (parent_id) on delete cascade; | 无 |
create index idx_mf_db_child_parent_id on mf_db_child(parent_id); | 在外键上建立索引. 避免主表更新时锁定整个子表 |