范式与反范式的应用(一)
在 数据库设计中范式的应用是一个永恒的话题,从一开始学关系型数据库设计开始,老师就会对我们说在进行数据库的表结构设计时,运用范式会有多么重要的意义, 确实,在实际工作当中你也会发现范式确实非常重要,但是随着工作的深入,你会慢慢发现有时候遵守范式反而会让你掉入一个又一个陷阱,于是我们又会谈到一个 反范式的概念,什么时候需要遵守范式,什么时候又需要反范式,笔者试图利用几年开发的经验,结合大众点评网的实际例子,来跟大家分享一下这个过程。
总体来说,对于一个公共型网站的数据库的设计往往会经历三个阶段,第一阶段:遵守范式;第二阶段:反范式;第三阶段:结合范式与反范式,灵活应用。
今天想先讲讲第一阶段,举几个点评网数据库设计的例子来讲一下如何来遵守范式进行数据库设计的。
第一范式(1NF)
所谓第一范式(1NF),是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。 如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含 一个实例的信息。
一般讲,只要是关系型数据库,一般都会遵守第一范式,但是事情往往没有那么简单,在进行数据库设计的时候,你真的都考虑清楚每个字段都是不可分割的 了吗?就拿点评网的商户信息表来说,有一个字段叫“商户名”,如果要添加两家新商户,比如“麻辣风暴”和“豆捞坊天目西路店”,那就会添加两条记录,其中 一条的商户名为“麻辣风暴”,而另外一条记录的商户名为“豆捞坊天目西路店”,那么这个就不符合第一范式了,因为其实这个信息是可以分割的,其中“豆捞 坊”是总店名,而“天目西路店”是分店名。所以根据第一范式的设计,应该分拆为两个字段即商户名和分店名。
第二范式(2NF)
所谓第二范式(2NF),是指数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选关键字。
这些定义总是非常的晦涩难懂,还是要靠举例说明才能比较清晰。
我们用点评网社区的部落成员表来举例,部落成员表的作用是定义每个部落的成员信息:
部落名称 会员名 会员等级 加入时间 部落角色
显然这个表有两个关键字段,即部落名称和会员名,其中会员等级这个非关键字段依赖于会员名这个关键字段,不以该会员是否加入该部落而改变,其性质不同于加入时间和部落角色,所以这样的设计是不符合第二范式的,为了遵守第二范式,我们需要将该表分拆为两个表:
会员ID 会员名 会员等级
部落名称 会员ID 加入时间 部落角色
第三范式(3NF)
在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如果存在"A → B → C"的决定关系,则C传递函数依赖于A。
还是拿点评网的商户信息表来举例,点评的商户一般都由会员来添加,这样商户信息表内就会包含以下信息:
商户名 分店名 会员名 会员等级
如果商户信息表是这样设计的,虽然会员名并非是一个关键字段,但是这不符合第三范式了,因为其中的会员等级两个字段和会员名这个字段有函数依赖的关系。所以根据第三范式,应该将这个表分拆为两个表,即商户信息表和会员信息表。
会员ID 会员名 会员等级
商户名 分店名 会员ID
希望以上的几个例子能帮助读者理解三个范式的含义,当然,当你的网站到了一定的规模以后,你会发现,完全按照这样的范式来进行设计,那你就被忽悠了,被忽悠了怎么办?请关注本博客,下次将通过这些例子的演化来说明什么情况下我们需要来反范式。
范式与反范式的应用(二)
前面详细的介绍了三个范式的概念 ,都是一些基本的概念,权当是一个铺垫,本文需要讲述的是在大众点评网的发展过程中,反范式的运用以及范式的回归。
反范式的应用
反范式的讨论在网上已经很多,之所以应用反范式,当然是为了查询的效率,当点评网成为众多喜欢美食的网友每天必上的网站之后,数据库增加了大量的数据,如果完全按照范式来进行设计,那查询将是致命的。
举个例子,要查询社区里面最新的10个帖子,如果按照范式设计,那么将关联两个表,一张是帖子表,另外一张是会员表,整个查询如下:
SELECT TOP 10 N .帖子标题, U .会员昵称, N .会员 ID FROM 帖子表 N
JOIN 会员表 U
ON N .会员 ID = U .会员 ID
ORDER BY N .帖子 ID DESC
这样的查询将产生两个聚集索引扫描,一个在帖子表上,另外一个在会员表上,然后再进行嵌套循环,当数据不多的时候,这样的查询没有问题,当两张 表都是百万数量级的时候,问题出现了,这个查询动不动就是几百毫秒,甚至更慢,这样的查询效率根本不能满足点评网对于网页速度的要求(一般不能超过100 毫秒),怎么办,当然要反范式,在帖子表里面添加冗余字段——会员昵称,这样我们就可以通过下面的查询达到同样的目的:
SELECT TOP 10 帖子标题, 会员昵称,会员 ID FROM 帖子表 ORDER BY 帖子 ID DESC
这个查询只需要通过一个聚集索引扫描就可以得到(用到哪个索引还会跟ORDER BY后面的字段有关,例子中用的是Primary Key作为排序的字段,所以用到的是聚集索引扫描),将两个查询放在一起查看执行计划,就会发现,第一个查询开销占了92%,而第二个才8%,也就是说, 第二个查询比起第一个查询,效率上优化了10倍以上,成果显著啊。
于是,在点评网的这个阶段,大量的冗余字段出现在表的设计当中,主要集中在会员的昵称以及商户的名称上,一方面,这些信息在其他应用中频繁的用到,另外一方面,这两个信息所在的主表都是特别大的表,做关联查询消耗太大,而且容易造成资源的争夺。
但是新的问题随之产生,每当一个会员去更新自己的昵称的时候,我们会执行一个存储过程,这个存储过程的目的就是去更新大量的会员昵称的冗余字 段,这些更新对于一个活跃会员来说,将是非常耗时的,因为需要更新的数据实在太多,而Web 2.0的精髓之一就是个性化,这样的设计对于个性化来说,有着不可调和的矛盾。于是,范式就像王者一样归来了。
王者归来
范式这个王者的归来,也是需要一定的时机的。
时机1:网站规模的发展,已经造成反范式的副作用越来越大;
时机2:公司规模的发展,已经有足够的财力建立更好的硬件平台;
于是,我们选择了Memcached , 一个分布式的缓存系统,我们将会员信息以实体类的方式保存在Memcached里面,只要是可序列化的数据,经过装箱和拆箱,都可以保存到 Memcached中并随时可以快速的访问到这些对象,Memcached可以解决大量数据的缓存并保持多台Web Server得到的缓存数据是一致的,于是,前面那个例子可以通过这样的查询来进行
SELECT TOP 10 帖子标题,会员 ID FROM 帖子表 ORDER BY 帖子 ID DESC
然后再通过缓存系统封装的批量读取的方法得到这些会员的昵称,再显示到网页上,于是,访问速度快了,冗余数据没了,只要有一套完善的缓存管理机 制,问题就迎刃而解了。有时候,你还会因为这样的查询得到一些惊喜,因为会员ID是一个整型的字段,当你用一些非PK的字段作为排序的字段时,可以把会员 ID包含在其索引里面(SQL Server 2005新特性),这样在索引扫描后可以避免一次聚集索引扫描,再一次的提高了查询效率。
相信对于这个问题的讨论是不会结束的,随着点评网的继续发展,新的问题必然还会产生,互联网就是要随需应变,到时候再来跟大家分享。