影响数据检索效率的几个因素

数据检索有两种主要形态。第一种是纯数据库型的。典型的结构是一个关系型数据,好比 mysql。用户经过 SQL 表达出所须要的数据,mysql 把 SQL 翻译成物理的数据检索动做返回结果。第二种形态是如今愈来愈流行的大数据玩家的玩法。典型的结构是有一个分区的数据存储,最初这种存储就是原始的 HDFS,后来开逐步有人在 HDFS 上加上索引的支持,或者干脆用 Elasticsearc 这样的数据存储。而后在存储之上有一个分布式的实时计算层,好比 Hive 或者 Spark SQL。用户用 Hive SQL 提交给计算层,计算层从存储里拉取出数据,进行计算以后返回给用户。这种大数据的玩法起初是由于 SQL 有不少 ad-hoc 查询是知足不了的,干脆让用户本身写 map/reduce 想怎么算均可以了。可是后来玩大了以后,愈来愈多的人以为这些 Hive 之类的方案查询效率怎么那么低下啊。因而一个又一个项目开始去优化这些大数据计算框架的查询性能。这些优化手段和经典的数据库优化到今天的手段是没有什么两样的,不少公司打着搞计算引擎的旗号干着从新发明数据库的活。因此,回归本质,影响数据检索效率的就那么几个因素。咱们不妨来看一看。html

数据检索干的是什么事情

定位 => 加载 => 变换java

找到所须要的数据,把数据从远程或者磁盘加载到内存中。按照规则进行变换,好比按某个字段group by,取另一个字段的sum之类的计算。node

影响效率的四个因素

  • 读取更少的数据
  • 数据本地化,充分遵循底层硬件的限制设计架构
  • 更多的机器
  • 更高效率的计算和计算的物理实现

原则上的四点描述是很是抽象的。咱们具体来看这些点映射到实际的数据库中都是一些什么样的优化措施。python

读取更少的数据

数据越少,检索须要的时间固然越少了。在考虑全部技术手段以前,最有效果的恐怕是从业务的角度审视一下咱们是否须要从那么多的数据中检索出结果来。有没有可能用更少的数据达到一样的效果。减小的数据量的两个手段,聚合和抽样。若是在入库以前把数据就作了聚合或者抽样,是否是能够极大地减小查询所须要的时间,同时效果上并没有多少差别呢?极端状况下,若是须要的是一天的总访问量,好比有1个亿。查询的时候去数1亿行确定快不了。可是若是统计好了一天的总访问量,查询的时候只须要取得一条记录就能够知道今天有1个亿的人访问了。mysql

索引是一种很是常见的减小数据读取量的策略了。通常的按行存储的关系型数据库都会有一个主键。用这个主键能够很是快速的查找到对应的行。KV存储也是这样,按照Key能够快速地找到对应的Value。能够理解为一个Hashmap。可是一旦查询的时候不是用主键,而是另一个字段。那么最糟糕的状况就是进行一次全表的扫描了,也就是把全部的数据都读取出来,而后看要的数据到底在哪里,这就不可能快了。减小数据读取量的最佳方案就是,创建一个相似字典同样的查找表,当咱们找 username=wentao 的时候,能够列举出全部有 wentao 做为用户名的行的主键。而后拿这些主键去行存储(就是那个hashmap)里捞数据,就一捞一个准了。算法

谈到索引就不得不谈一下一个查询使用了两个字段,如何使用两个索引的问题。mysql的行为能够表明大部分主流数据库的处理方式:
https://www.percona.com/blog/2012/12/14/the-optimization-that-often-is...
https://www.percona.com/blog/2014/01/03/multiple-column-index-vs-multi...
基本上来讲,经验代表有多个单字段的索引,最后数据库会选一最优的来使用。其他字段的过滤仍然是经过数据读取到内存以后,用predicate去判断的。也就是没法减小数据的读取量。
在这个方面基于inverted index的数据就很是有特色。一个是Elasticsearch为表明的lucene系的数据库。另一个是新锐的druid数据库。
https://www.found.no/foundation/elasticsearch-from-the-bottom-up/
http://druid.io/blog/2012/09/21/druid-bitmap-compression.html
效果就是,这些数据库能够把单字段的filter结果缓存起来。多个字段的查询能够把以前缓存的结果直接拿过来作 AND 或者 OR 操做。sql

索引存在的必要是由于主存储没有提供直接的快速定位的能力。若是访问的就是数据库的主键,那么须要读取的数据也就很是少了。另一个变种就是支持遍历的主键,好比hbase的rowkey。若是查询的是一个基于rowkey的范围,那么像hbase这样的数据库就能够支持只读取到这个范围内的数据,而不用读取再也不这个范围内的额外数据,从而提升速度。这种加速的方式就是利用了主存储自身的物理分布的特性。另一个更常见的场景就是 partition。好比 mysql 或者 postgresql 都支持分区表的概念。当咱们创建了分区表以后,查找的条件若是能够过滤出分区,那么能够大幅减小须要读取的数据量。比 partition 更细粒度一些的是 clustered index。它其实不是一个索引(二级索引),它是改变了数据在主存储内的排列方式,让相同clustered key的数据彼此紧挨着放在一块儿,从而在查询的时候避免扫描到无关的数据。比 partition 更粗一些的是分库分表分文件。好比咱们能够一天创建一张表,查询的时候先定位到表,再执行 SQL。好比 graphite 给每一个 metric 建立一个文件存放采集来的 data point,查询的时候给定metric 就能够定位到一个文件,而后只读取这个文件的数据。数据库

另外还有一点就是按行存储和按列存储的区别。按列存储的时候,每一个列是一个独立的文件。查询用到了哪几个列就打开哪几个列的文件,没有用到的列的数据碰都不会碰到。反观按行存储,一张中的全部字段是彼此紧挨在磁盘上的。一个表若是有100个字段,哪怕只选取其中的一个字段,在扫描磁盘的时候其他99个字段的数据仍然会被扫描到的。缓存

考虑一个具体的案例,时间序列数据。如何使用读取更少的数据的策略来提升检索的效率呢?首先,咱们能够保证入库的时间粒度,维度粒度是正好是查询所须要的。若是查询须要的是5分钟数据,可是入库的是1分钟的,那么就能够先聚合成5分钟的再存入数据库。对于主存储的物理布局选择,若是查询老是针对一个时间范围的。那么把 timestamp 作为 hbase 的 rowkey,或者 mysql 的 clustered index 是合适。这样咱们按时间过滤的时候,选择到的是一堆连续的数据,不用读取以后再过滤掉不符合条件的数据。可是若是在一个时间范围内有不少中数据,好比1万个IP,那么即使是查1个IP的数据也须要把1万个IP的数据都读取出来。因此能够把 IP 维度也编码到 rowkey 或者 clustered index 中。可是假如另外还有一个维度是 OS,那么查询的时候 IP 维度的 rowkey 是没有帮助的,仍然是要把全部的数据都查出来。这就是仅依靠主存储是没法知足各类查询条件下都可以读取更少的数据的缘由。因此,二级索引是必要的。咱们能够把时间序列中的全部维度都拿出来创建索引,而后查询的时候若是指定了维度,就能够用二级索引把真正须要读取的数据过滤出来。可是实践中,不少数据库并不由于使用了索引使得查询变快了,有的时候反而变得更慢了。对于 mysql 来讲,存储时间序列的最佳方式是按时间作 partition,不对维度创建任何索引。查询的时候只过滤出对应的 partition,而后进行全 partition 扫描,这样会快过于使用二级索引定位到行以后再去读取主存储的查询方式。究其缘由,就是数据本地化的问题了。ruby

数据本地化

数据本地化的实质是软件工程师们要充分尊重和理解底层硬件的限制,而且用各类手段规避问题最大化利用手里的硬件资源。本地化有不少种形态

  • 最多见的最好理解的本地化问题是网络问题。咱们都知道网络带宽不是无限的,比本地磁盘慢多了。若是可能尽可能不要经过网络去访问数据。即使要访问,也应该一次抓取多一些数据,而不是一次搞一点,而后搞不少次。由于网络链接和来回的开销是很是高的。这就是 data locality 的问题。咱们要把计算尽量的靠近数据,减小网络上传输的数据量。
  • 这种带宽引发的本地化问题,还有不少。网络比硬盘慢,硬盘比内存慢,内存比L2缓存慢。作到极致的数据库可让计算彻底发生在 L2 缓存内,尽量地避免频繁地在内存和L2之间倒腾数据。
  • 另一种形态的问题化问题是磁盘的顺序读和随机读的问题。当数据彼此靠近地物理存放在磁盘上的时候,顺序读取一批是很是快的。若是须要随机读取多个不连续的硬盘位置,磁头就要来回移动从而使得读取速度快速降低。即使是 SSD 硬盘,顺序读也是要比随机读快的。

基于尽量让数据读取本地化的原则,检索应该尽量地使用顺序读而不是随机读。若是能够的话,把主存储的row key或者clustered index设计为和查询提交同样的。时间序列若是都是按时间查,那么按时间作的row key能够很是高效地以顺序读的方式把数据拉取出来。相似地,按列存储的数据若是要把一个列的数据都取出来加和的话,能够很是快地用顺序读的方式加载出来。

二级索引的访问方式典型的随机读。当查询条件通过了二级索引查找以后获得一堆的主存储的 key,那么就须要对每一个 key 进行一次随机读。即使彼此仅靠的key能够用顺序读作一些优化,整体上来讲仍然是随机读的模式。这也就是为何时间序列数据在 mysql 里创建了索引反而比没有建索引还要慢的缘由。

为了尽量的利用顺序读,人们就开始想各类办法了。前面提到了 mysql 里的一行数据的多个列是彼此紧靠地物理存放的。那么若是咱们把所须要的数据建成多个列,那么一次查询就能够批量得到更多的数据,减小随机读取的次数。也就是把以前的一些行变为列的方式来存放,减小行的数量。这种作法的经典案例就是时间序列数据,好比能够一分钟存一行数据,每一秒的值变成一个列。那么行的数量能够变成以前的1/60。

可是这种行变列的作法在按列存储的数据库里就不能直接照搬了,有些列式数据库有column family的概念,不一样的设置在物理上存放多是在一块儿的也多是分开的。对于 Elasticsearch 来讲,要想减小行的数量,让一行多pack一些数据进去,一种作法就是利用 nested document。内部 Elasticsearch 能够保证一个 document 下的全部的 nested document是物理上靠在一块儿放在同一个 lucene 的 segment 内。

网络的data locality就比较为人熟知了。map reduce的大数据计算模式就是利用map在数据节点的本地把数据先作一次计算,每每计算的结果能够比原数据小不少。而后再经过网络传输汇总后作 reduce 计算。这样就节省了大量网络传输数据的时间浪费和资源消耗。如今 Elasticsearch 就支持在每一个 data node 上部署 spark。由 spark 在每一个 data node 上作计算。而不用把数据都查询出来,用网络传输到 spark 集群里再去计算。这种数据库和计算集群的混合部署是高性能的关键。相似的还有 storm 和 kafka 之间的关系。

网络的data locality还有一个老大难问题就是分布式大数据下的多表join问题。若是只是查询一个分布式表,那么把计算用 map reduce 表达就没有多大问题了。可是若是须要同时查询两个表,就意味着两个表可能不是在物理上一样均匀分布的。一种最简单的策略就是找出两张表中最小的那张,而后把表的内容广播到每一个节点上,再作join。复杂一些的是对两个单表作 map reduce,而后按照相同的 key 把部分计算的结果聚集在一块儿。第三种策略是保证数据分布的方式,让两张表查询的时候须要用到的数据总在一块儿。没有完美的方案,也不大可能有完美的方案。除非有一天网络带宽能够大到忽略不计的地步。

更多的机器

这个就没有什么好说的了。多一倍的机器就多一倍的 CPU,能够同时计算更多的数据。多一倍的机器就多一倍的磁头,能够同时扫描更多的字节数。不少大数据框架的故事就是讲如何如何经过 scale out 解决无限大的问题。可是值得注意的是,集群能够无限大,数据能够无限多,可是口袋里的银子不会无限多的。堆机器解决问题比升级大型机是要便宜,可是机器堆多了也是很是昂贵的。特别是 Hive 这些从一开始就是分布式多机的检索方案,刚开始的时候效率并不高。堆机器是一个乘数,当数据库原本单机性能不高的时候,乘数大并不能起到决定性的做用。

更高效的计算和计算实现

检索的过程不只仅是磁盘扫描,它还包括一个可简单可复杂的变换过程。使用 hyperloglog,count min-sketch等有损算法能够极大地提升统计计算的性能。数据库的join也是一个常常有算法创新的地方。
计算实现就是算法是用C++实现的仍是用java,仍是python实现的。用java是用大Integer实现的,仍是小int实现的。不一样的语言的实现方式会有一些固定的开销。不是说快就必定要C++,可是 python 写 for 循环是显然没有期望的。任何数据检索的环节只要包含 python/ruby 这些语言的逐条 for 循环就必定快不起来了。

结论

但愿这四点能够被记住,成为一种指导性的优化数据检索效率的思惟框架。不管你是设计一个mysql表结构,仍是优化一个spark sql的应用。从这四个角度想一想,都有哪些环节是在拖后腿的,手上的工具备什么样的参数能够调整,让随机读变成顺序读,表结构怎么样设计能够最小化数据读取的量。要作到这一点,你必须很是很是了解工具的底层实现。而不是盲目的相信,xx数据库是最好的数据库,因此它必定很快之类的。若是你不了解你手上的数据库或者计算引擎,当它快的时候你不知道为什么快,当它慢的时候你就更加无从优化了。

相关文章
相关标签/搜索