MongoDB性能优化——范式化与反范式
-
一、概述
从性能优化的角度来看,对于集合的设计,我们需要考虑的是集合中数据的常用操作,例如我们需要设计一个日志(log)集合,日志的查看频率不高,但写入频率却很高,那么我们就可以得到这个集合中常用的操作是更新(增删改)。而对于一个查看频率很高,但写入频率很低的集合,那么它的常用操作就是查询。
范式化(normalization)是关系模型的发明者埃德加•科德于1970年提出这一概念,范式化会将数据分散到不同的表中,利用关系模型进行关联,由此带来的优点是,在后期进行修改时,不会影响到与其关联的数据,仅对自身修改即可完成。
反范式化(denormalization)是针对范式化提出的相反理念,反范式化会将当前文档的数据集中存放在本表中,而不会采用拆分的方式进行存储。
所以对于集合频繁更新或者频繁查询,我们能否合理运用范式化与反范式化,对于性能的提高至关重要。从而我们应该怎么灵活去设计和使用它们呢?接下来举几个例子跟大家说明下具体情况。
二、实际应用中的3种形式
- 1、完全分离(范式化设计)
假设现在我们需要存储一本图书及其作者,我们将作者的id数组作为一个字段添加到了图书中去,代码如下:
{ "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "MongoDB性能优化", "author" : [ ObjectId("144b5d83041c7dca84416"), ObjectId("144b5d83041c7dca84418"), ObjectId("144b5d83041c7dca84420"), ] }
在MongoDB中用存储主键的方式进行关联查询,当我们想查询某位作者个人的所有信息,那么首先要查询所有图书,再从图书中获得作者id,最后查询到作者。显然这样的查询性能是不理想的,但是当我们需要修改某位作者信息的时候,则无需考虑此作者集合关联的图书,直接修改此作者的字段即可。如此看来,更新效率是最高的,但查询效率是最低的。
- 2、完全内嵌(反范式化设计)
我们将作者的字段完全嵌入到了图书中去,代码如下:
{ "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "MongoDB性能优化", "author" : [ { "name" : "Plant" "age" : 20, "nationality" : "CHINA", }, { "name" : "Henry" "age" : 21, "nationality" : "CHINA", }, { "name" : "Kevin" "age" : 22, "nationality" : "CHINA", }, ] }
这种情况在查询的时候,直接查询图书即可获得所对应作者的全部信息,但因一个作者可能有多本著作,当修改某位作者字段信息时,我们需要遍历所有图书以找到该作者并将其修改。
- 3、部分内嵌(折中方案)
这次我们将作者字段中的最常用的一部分提取出来,当我们只需要获得图书和作者名时,无需再次进入作者集合进行查询,仅在图书集合查询即可获得,代码如下:
{ "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "MongoDB性能优化", "author" : [ { "_id" : ObjectId("144b5d83041c7dca84416"), "name" : "Plant" }, { "_id" : ObjectId("144b5d83041c7dca84418"), "name" : "Henry" }, { "_id" : ObjectId("144b5d83041c7dca84420"), "name" : "Kevin" }, ] }
这就是一种相对折中的方式,既保证了查询效率,也保证的更新效率。但这样的方式显然要比前两种较难以掌握,难点在于需要与实际业务进行结合来寻找合适的提取字段。如上所述,名字显然不是一个经常修改的字段,这样的字段如果提取出来是没问题的,但如果提取出来的字段是一个经常修改的字段(比如age)的话,我们依旧在更新这个字段时需要大范围的寻找并进行更新。
三、小结
- 综上所述,其实范式化和反范式化之间不存在优劣的问题。范式化的好处是可以在我们写入、修改、删除时提供更高性能,而反范式化可以提高我们在查询时的性能。所以在实际的工作中我们需要根据自己实际的需要来设计文档中的字段,以获得最高的效率。因此,在项目设计阶段,明确集合的用途是对性能调优非常重要的一步。