这是一篇2年多以前带过的实习生 Peflapos 所整理的小文,拿出来缅怀下,也希望他在新的公司一切顺利。
对1亿条 K-V 型记录查询效率的验证
关于 K-V 型存储
1. 为什么考虑 K-V 存储形式?
假设有一个 Request 类如下:
public class Request { public Guid RequestId {get; set;}; public string RequestName {get; set;}; public string RequestOwner {get; set;}; }
我们可以按照如下的结构把它存入数据库:
RequestId | RequestName | RequestOwner |
2E714306-5A42-4C10-8E4D-630BB2D3E089 | Create site | peflapos |
33566512-8003-4783-ABEB-FFFFC7A11CA0 | Change permission | Bob |
将 RequestId 列上添加主键,唯一标识一条记录。这是比较常见的做法。但是假如 Request 类是一个总在变化的类,总是添加或删除一些现有的属性,该怎么办呢?如果仍旧采用上述的方式存储数据,那就要改变数据库的表的结构,例如添加一列。我们知道,类与数据库是有映射关系的,改变其一就要重新维护映射关系。实际开发的过程中,若是改动了数据库,需要改动的往往不止这些,可谓是牵一发而动全身。既然这样,我们不妨考虑改变一下上述表结构:
RequestId | Keys | Value |
2E714306-5A42-4C10-8E4D-630BB2D3E089 | requestName | Create site |
2E714306-5A42-4C10-8E4D-630BB2D3E089 | requestOwner | peflapos |
33566512-8003-4783-ABEB-FFFFC7A11CA0 | requestName | Change permission |
33566512-8003-4783-ABEB-FFFFC7A11CA0 | requestOwner | Bob |
新的表结构看上去是解决了因为改变数据库表结构而带来的一系列问题,但是我们发现这样做的话,RequestId 字段势必会有重复的值出现,这样就不能建立主键了。那么这样的结构会不会对数据操作的效率产生影响呢?或者是有什么方法能使这种结构的表在操作数据的时候,达到一个比较理想的效果呢?在这里我们一起来做一下比较。
2. 分析之前的准备
因为 RequestId 字段有重复的值,不能建立主键,为了比较主键对表的影响,这里多添加了一列 Id。并且准备了两张表,两表的唯一区别是一个表的 Id 列上有主键,一个上没有,其中 RequestId 每 200 个重复一次,即相当于一个 Request 里有 200 个属性,表的详细结构如下:
Column Name | Data Type | Allow Nulls | Description |
countNumber | int | No | 标记行号,方便测试 |
id | uniqueidentifier | Yes/No | 作为测试主键用的列 |
keys | uniqueidentifier | Yes | 对应的属性名 |
value | nvarchar(Max) | Yes | 对应的属性值 |
requestId | uniqueidentifier | Yes | 类的唯一标识 |
K-V型记录——硬盘存储
1. 下面的图表反映了添加主键与不添加主键时,数据库文件的物理存储大小。在本次比较中,主键会多占大约 29.8% 的硬盘空间。其中柱状图中的数字的单位为 MB。
图1 主键对硬盘存储的影响-1
2. 下表反映出,记录数与所占磁盘空间基本是呈线性关系的,在本次测试中,平均每十万条带主键的记录要比不带主键的记录大约多占了 4MB 磁盘空间。
图2 主键对硬盘存储的影响-2
K-V 型记录——效率
1.更新数据的效率
下面的图表反映出了表上有主键与没主键时,更新时间的比较(这里不使用id列作为条件,因为真实的环境中是没有 Id 列的,这里的id列只是为了完成测试。在这里使用 RequestId 与 Keys 唯一确定一条记录后,进行修改)。通过比较我们发现,带有主键的表的更新效率反而大大下降,所有的更新操作都超过了1分钟。而不带主键的表,最慢的也只要 27 秒。
图3 主键对更新效率的影响
2.查询数据的效率
下面的图表反映了带有主键与不带主键时,查询一条记录所花费的时间的比较。为了更加贴近真实环境。这里仍然不用主键所在的字段作为查询的条件。通过比较我们发现,这种情况下,主键反而大大影响了查询效率。
图4 主键对查询效率的影响
K-V 型记录——添加索引
1.添加索引的理由
普通的表中,操作数据的时间超过了 20 秒,虽然优于主键,但是给用户的体验仍旧很糟糕,而主键在这里又起不到什么很好的效果,因此我们考虑建立索引。在当前的 K-V 型数据里,能唯一标识一个 Request 类的字段是 RequestId,它在数据库中是重复出现的,既然不能设置主键,我们可以考虑设置索引。索引分为聚集索引和非聚集索引,这里我们在 RequestId 上设置一个聚集索引。使用聚集索引的理由是比较节省存储空间,虽然一个表中只能有一个聚集索引,但是根据实际情况,只要有这么一个索引就可以了。下面的图表比较了主键、聚集索引和普通表所占的存储空间,数据量是一亿条。同样,柱状图上面的数字的单位依旧是MB。通过比较我们发现与主键相比,聚集索引也会节省一些存储空间。
图5 索引、主键对硬盘存储的影响
K-V 型记录——索引效率
1.索引对查询效率的影响
首先用一个 RequestId 搜索出一个 Request 中包含的全部 200 个属性,可以发现,效果还是很客观的,几乎不需要等待。
图6 索引对数据查询效率的影响-1
下面看看查询某个 Request 中的某一个属性:
图7 索引对数据查询效率的影响-2
效果也很乐观,几乎也不需要等待。更新一条记录试试,通过实践告诉我们,这个操作也是几乎不用等待的。
图8 索引对数据更新效率的影响
2. 索引对插入效率的影响
建立索引以后,插入一条记录就要维护一次索引,那么索引对插入记录的效率有着怎样的影响呢?下面的这个测试针对插入数据而进行的,比较了带有索引与不带有索引的情况下,一分钟可以入库多少条记录。也比较了单线程,5个线程,10个线程等多线程操作时入库的记录数:
图9 索引对数据插入效率的影响
通过比较发现,建立索引会使插入数据的效率有所下降,但是当线程比较多的时候,下降的不是很明显。与没有索引时相比,不会差的很多。
3.大量数据时索引的效率:
下面准备了一张带有 10 亿记录的表。仍旧在 RequestId 上建立聚集索引。
图10 表 TestTableTwo 的记录个数
图11 基于大量数据的索引对数据查询效率的影响-1
图12 基于大量数据的索引对数据查询效率的影响-2
图13 基于大量数据的索引对数据更新效率的影响
小结
通过很多的对比,我们发现对于 K-V 型记录,建立索引可以很好的改善查询效率。即使数据量达到 10 亿级别时,查询一条记录也几乎是不用等待的,也就是说数据量很大的时候,索引依旧能很好的发挥它的性能,而且真实环境中,客户的数据可能达不到 10 亿。在这次测试中,对于10亿条记录,建立聚集索引会多消耗 35G 存储空间,但是能在性能上提高很多,也算是用空间换取时间吧。35G 的硬盘存储也不会多花费客户太多的资金,若是真的能带来很好的体验,想必客户也会接受的吧。