简介
在学校的时候就看过很多关于数据库设计的书,也看过很多博客分享的他们数据库设计的技巧和规范。但是真正到了公司的时候才发现更多的时候不会有太多的时间去做一个完美的设计,很多数据约束是没有办法确定,很多业务的约束也没有办法得到。不是技术上办不到,很多时候和业务相关的,必须要在业务流程上做约束。
就一个简单的例子,用户名在做数据库设计的时候采用什么数据类型?数据长度设计为多大?这些都需要业务上做约束的,如果产品不能给业务约束,能怎么设计?很多时候我们就看到了一个用户名设计成了varchar(200),但是这样真的没有什么问题吗?一个问题是没有业务约束前端可能不会做检查,必须服务器端检查。另一个问题是如果这个表非常大,有1亿用户,而用户表中有很多不能再业务上做约束的,所以大量存在varchar(200)这种情况存在怎么办?
所以设计往往不是一个人的事情,还要和产品、运营、其他的使用者的沟通上,很多时候只需要和产品沟通,但是如果一个不给力的产品,很多需要设计者去和更多的相关人员沟通和协调。
很多时候不是技术上不能做的不好,而是和整个团队相关,很多时候老板只是给一个想法要做什么,然后就要求什么时候必须上线。什么具体的需求到没有,原型也没有,更不要说业务约束了,怎么设计?
上面扯的有一点远了啊,只是感叹一下有时候人在江湖,身不由己。有人的地方就有江湖。所以如果你觉得某个app做的烂,不要只骂技术。锅不能一个人背啊,是吧。
下面就以一个简单的用户表设计为例介绍一下怎样尽量根据已有的条件约束尽量做好数据库的设计,特别是一些重要的业务,不说做的完美,但是至少够用是吧。
表数据类型选择
很多刚刚开始做数据库设计的时候基本做个像上面的设计吧,这样有什么问题呢?很多varchar(255)这样的数据,查询很难优化,另外数据库是按行读取的,也多的varchar这样的数据,读取的数据量就也多。而IO操作是非常耗时的,而很多操作是需要读取大量行的。
当然有一些存储引擎对这个做了优化。例如MySQL的"overflow page"。你也可能说我们的查询都是设计了索引,都是走索引,只需要读取很少的行,所以不存在大量的IO操作。但是你能保证你们的需求不变吗?每一次需求变更你都有时间和耐心去优化你的索引和查询。所以最好的方式就是在设计上就解决一部分问题。
对于上图terrible_design_user中的很多列都可以改造。例如name就可以通过业务约束来限制,没有必要弄成varchar(255),你想你有注册的账号是255个字符的吗?如果每一次登录要输入255个字符你会崩溃吗?其实很多时候15个字符都已经是极限了,2句七言加一个符号也就15个字符,很多流行语也不会超过15个字符。所以name完全可以设计为varchar(15)或者varchar(20)。
对于password很多同学可能就没有什么好的办法了,因为很多人密码可能会非常长。这里就有一个非常小的技巧可以利用使用hash函数加密。数据库中也不可能存明文。例如使用sha256加密,这样只需要把password设计为固定的char(64)就可以了。
像portrait头像这种url类型的的确是不好设计的。这里的头像url还好,因为这个url可以自己控制,完全就可以使用相对路径,存文件名就可以了。在应用中来拼接url。
像age这种就可以使用tinyint unsigned,当然使用int也没有关系,对于现在的机器可以任性的说没有关系,哥能耗的起,土豪就是任性。
像sex这种性别类型,可以选择enum枚举类型,也可以使用tinyint看设计上的决定和规范,尽量和系统的其他设计风格保持一致。
像phone电话号码这种没得说char(11),不用非得弄个char(20),当然也得考虑你的实际情况,是不是需要加国际区号,加区号要怎么处理和设计。对于手机号码其实还可以考虑int unsigned类型。手机号码都是以1开头的11位,int类型完全可以放下。
还有就是对于自增主键id的设计,一般使用int unsigned就可以了。没有必要弄成long unsigned,因为int unsigned在40亿+,如果数据量在40亿+,早就应该考虑分表了,单表40亿数据可不是什么好的设计。有一种特殊情况,这个表是经常插入和删除的,例如在分布式的应用中经常使用到的消息状态表,这个表的本身数据可能不多,但是id会一直自增,这里就可以使用long unsigned类型。
为了更好的设计数据库,选择更加适合的数据类型,就必须对数据库提供的类型比较熟悉,下面就是MySQL常用数据类型:
表的垂直拆分
这里表的拆分指的是表的垂直拆分,对表垂直拆分的目的是让表更加的小,有更快的查询速度,保证核心业务更快的响应。例如对于上面的用户表我们就可以做垂直拆分,拆分为下面的3个表:
记住,我们垂直拆分表的目的是为了让核心业务用更快的查询速度,对于用户表最重要的操作当然是用户登录,获取用户基本信息了,当然这和具体的业务相关。原则就是尽量让你的最常用的表尽量小。一般来说,对于用户表最常用的数据就是你在用户登录后首页展示出来的数据,一般需要的就是昵称,头像,可能还有一些积分,金币简单数据。这些数据尽量少,尽量放在一个表中不要做连表查询。
例如上面拆分之后的用户表就非常的精简了,只保留了用户名和密码用做登录唯一标识,状态标识用户是否可用,头像和昵称用于首页显示。
我们把业务场景应用较少的用户信息拆分到了USER_INFO表中,把应用场景更少的安全问题拆分到了USER_SECURITY_QUESTION中。
附录
CREATE TABLE `USER` ( `id` int unsigned PRIMARY KEY auto_increment NOT NULL, `name` varchar(20) NOT NULL COMMENT '用户名,字母数字中文', `password` char(64) NOT NULL COMMENT '密码,sha256加密', `nick_name` varchar(20) DEFAULT '' COMMENT '昵称', `portrait` varchar(30) DEFAULT '' COMMENT '头像,使用相对路径', `status` enum('valid','invalid') DEFAULT 'valid' COMMENT 'valid有效,invalid无效') ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';CREATE TABLE `USER_INFO` ( `id` int unsigned PRIMARY KEY auto_increment NOT NULL, `user_id` int NOT NULL, `sex` enum('male','famale') DEFAULT 'male' COMMENT 'male男,famale女', `age` tinyint(3) unsigned DEFAULT NULL COMMENT '年龄', `phone` char(11) DEFAULT NULL COMMENT '手机', `address` varchar(255) DEFAULT NULL COMMENT '地址', `register_time` datetime DEFAULT NULL COMMENT '注册时间', `last_login` datetime DEFAULT NULL COMMENT '上一次登陆时间') ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';CREATE TABLE `USER_SECURITY_QUESTION` ( `id` int unsigned PRIMARY KEY auto_increment NOT NULL, `user_id` int NOT NULL COMMENT '用户id', `security_question` varchar(255) DEFAULT NULL COMMENT '保密问题', `security_question_answer` varchar(255) DEFAULT NULL COMMENT '保密问题答案') ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户保密问题表';