数据检索主要有两种形式。第一种是纯数据库类型。一个典型的结构是关系型数据,比如mysql。用户通过SQL表达需要的数据,mysql将SQL翻译成物理的数据检索动作并返回结果。第二种形式是现在越来越流行的大数据玩家玩的游戏。典型的结构是分区数据存储。起初,这个存储是原来的HDFS。后来逐渐有人在HDFS中加入索引支持,或者干脆使用Elasticsearch等数据存储。然后是在存储之上的分布式实时计算层,比如Hive或者SparkSQL。用户使用HiveSQL向计算层提交数据,计算层从存储中拉取数据计算后返回给用户。这种大数据的方式本来就是因为SQL有很多ad-hoc查询不能满足,所以用户可以自己简单的写map/reduce,想干什么就干什么。但是玩大了之后,越来越多的人觉得这些Hive等方案的查询效率这么低。于是一个接一个的项目开始优化这些大数据计算框架的查询性能。这些优化方法与直到今天的经典数据库优化方法没有什么不同。许多公司打着计算引擎的旗号重塑数据库。因此,回归本质,影响数据检索效率的因素无外乎几个。让我们来看看。数据检索是做什么的Locate=>Load=>Transform找到需要的数据,然后从远程或者磁盘上加载数据到内存中。按照规则进行转换,比如按某个字段分组,计算另一个字段的总和。影响效率的四大因素少读数据数据本地化,完全遵循底层硬件设计架构的约束更多机器更高效的计算和计算的物理实现原则上的四点描述非常抽象。我们来看看将这些点映射到实际数据库的优化措施。读取更少的数据拥有的数据越少,检索数据所需的时间就越少。在考虑所有技术手段之前,最有效的可能是从业务角度看我们是否需要从如此多的数据中检索结果。是否可以用更少的数据达到同样的效果。减少数据量的两种手段,聚合和抽样。如果数据在入库前进行聚合或者采样,是不是可以大大减少查询所需的时间,而且效果相差不大?极端情况下,如果你需要一天的总访问量,比如有1亿。查询的时候统计1亿行肯定不会很快。但是如果统计一天的总访问量的话,查询的时候只需要得到一条记录就知道今天有1亿人访问了。索引是一种非常常见的减少数据读取量的策略。一般按行存储的关系型数据库都会有一个主键。使用这个主键可以非常快速的找到对应的行。KV存储也是如此。可以根据Key快速找到对应的Value。可以理解为一个Hashmap。但是一旦查询,就不是用主键了,而是另外一个字段。那么最坏的情况就是进行全表扫描,也就是把所有的数据都读一遍,然后看需要的数据在哪,不能快。减少读取数据量的最佳解决方案是创建一个类似字典的查找表。当我们找到username=wentao时,我们可以列出所有以wenttao为用户名的行的主键。然后用这些主键在行存(也就是hashmap)中查找数据,一次就能找到一个。说到索引,就不得不说到一个查询使用两个字段时,如何使用两个索引。mysql的行为可以代表大部分主流数据库的处理方式:https://www.percona.com/blog/2012/12/14/the-optimization-that-ofen-is...https://www.percona.com/blog/2014/01/03/multiple-column-index-vs-multi...基本上,经验表明单字段索引有多个,***数据库会选择一个使用。其他字段的过滤还是在数据读入内存后通过predicate判断。即无法减少读取的数据量。在这方面,基于倒排索引的数据非常有特点。一种是以Elasticsearch为代表的Lucene数据库。另一个是尖端的德鲁伊数据库。https://www.found.no/foundation/elasticsearch-from-the-bottom-up/http://druid.io/blog/2012/09/21/druid-bitmap-compression.html效果就是这些databases可以缓存单个字段的过滤结果。多个字段的查询可以直接取之前缓存的结果进行AND或OR运算。索引是必需的,因为主存储不提供直接、快速的定位能力。如果访问的是数据库的主键,那么需要读取的数据就很少。另一种变体是支持遍历的主键,比如hbase的rowkey。如果查询是基于一个rowkey范围,那么像hbase这样的数据库可以只支持读取这个范围内的数据,而不需要再读取已经不在这个范围内的数据,从而提高速度。这种加速方式利用了主存本身物理分布的特点。另一个更常见的场景是分区。例如,mysql或postgresql都支持分区表的概念。当我们建立好分区表之后,如果搜索条件能够过滤掉分区,那么需要读取的数据量就可以大大减少。聚簇索引比分区更细粒度。它实际上不是索引(二级索引)。它改变了数据在主存中的排列方式,使相同聚簇键的数据并排放置,避免查询时扫描不相关的数据。比分区更厚的是分库分表分文件。比如我们可以一天建一个表,查询的时候,先定位到表,再执行SQL。例如,Graphite为每个指标创建一个文件来存储收集到的数据点。查询时,给定的metric可以定位到一个文件,然后只读取这个文件的数据。#p#还有一点就是按行存储和按列存储的区别。按列存储时,每一列都是一个独立的文件。打开查询中使用了哪些列的文件,没有使用的列的数据不会被触及。另一方面,它是按行存储的,一个工作表中的所有字段在磁盘上都是相邻的。如果一个表有100个字段,即使只选中其中一个字段,扫描磁盘时仍然会扫描剩下的99个字段的数据。考虑一个具体案例,时间序列数据。如何使用少读数据的策略来提高检索效率呢?首先,我们可以保证存储的时间粒度,维度粒度正是查询所需要的。如果查询需要5分钟的数据,但是数据库中存储的数据是1分钟的,那么可以聚合成5分钟的数据,然后存储到数据库中。对于主存物理布局的选择,如果查询总是针对一个时间范围。那么用timestamp作为hbase的rowkey比较合适,或者mysql的聚簇索引。这样我们在按时间过滤的时候,就选择了一堆连续的数据,读完就不需要过滤掉不合格的数据了。但是如果一个时间范围内的数据很多,比如10000个IP,那么即使要查看1个IP的数据,也需要读取10000个IP的所有数据。因此IP维度也可以被编码到rowkey或者聚簇索引中。但是如果还有一个维度是OS,那么IP维度的rowkey在查询的时候就没有帮助了,还是得把所有的数据都check出来。这就是为什么仅仅依靠主存储是不可能在各种查询条件下读取较少数据的原因。因此,二级索引是必要的。我们可以把时间序列中的所有维度都取出来建立一个索引,然后如果查询的时候指定了维度,我们就可以使用二级索引来过滤掉真正需要读取的数据。但在实践中,很多数据库并没有因为使用了索引而使查询变快,有时反而变慢了。对于mysql来说,最好的存储时间序列的方法是按时间分区,不在维度上创建任何索引。查询时只过滤掉对应的分区,然后进行全分区扫描,比使用二级索引定位行再读取主存的查询方式更快。原因是数据本地化的问题。数据本地化数据本地化的本质是软件工程师要充分尊重和理解底层硬件的局限性,利用各种手段避免问题,最大限度地利用手头的硬件资源。本地化有多种形式。最常见的本地化问题是网络问题。我们都知道网络带宽不是最优的,比本地磁盘慢很多。如果可能,尽量不要通过网络访问数据。即使要访问,也应该一次抓取更多的数据,而不是一次做一点点,然后做很多次。因为网络连接和来回的开销非常大。这就是数据局部性的问题。我们希望让计算尽可能接近数据,以减少网络上传输的数据量。此带宽会导致更多本地化问题。网络比硬盘慢,硬盘比内存慢,内存比二级缓存慢。优化后的数据库可以让计算完全发生在二级缓存中,尽可能避免数据在内存和二级缓存之间频繁的折腾。问题化问题的另一种形式是磁盘的顺序读取和随机读取问题。当数据在物理上彼此靠近存储在磁盘上时,顺序读取批次非常快。如果需要随机读取多个不连续的硬盘位置,磁头会来回移动,读取速度会迅速下降。即使对于SSD硬盘驱动器,顺序读取也比随机读取更快。基于尽可能使数据读取本地化的原则,检索应尽可能使用顺序读取而不是随机读取。如果可能,将主存的行键或聚簇索引设计成与查询提交相同。如果时间序列都是按时间查的,那么按时间做rowkey可以非常高效的按顺序拉出数据。同理,如果需要将列中存储的数据取出来相加,通过顺序读取可以非常快速地加载。二级索引的访问方式是典型的随机读。当通过二级索引查找查询条件,得到主存的一堆key时,需要对每个key进行随机读取。相互依赖的键即使可以通过顺序读取进行优化,但总体上还是处于随机读取模式。这就是为什么在mysql中对时序数据建立索引比不建立索引要慢的原因。为了尽可能地利用顺序阅读,人们开始想各种办法。前面说过,mysql中一行数据的多个列在物理上是相邻存储的。那么如果我们将需要的数据构建成多列,那么一次查询就可以批量获取更多的数据,减少随机读取的次数。也就是把之前的一些行改成列来存储,减少行数。这种方法的经典案例是时间序列数据。比如每分钟可以存一行数据,每一秒的值就变成一列。那么行数可以变成之前的1/60。但是,这种行转列的方法在列式数据库中是不能直接复制的。一些基于列的数据库有列族的概念,不同的设置可能在物理上存储在一起或分开。对于Elasticsearch,为了减少行数,让一行可以打包更多的数据,一种方式是使用嵌套文档。在内部,Elasticsearch可以确保一个文档下的所有嵌套文档在物理上靠得很近,并且放在同一个lucene段中。网络的数据局部性是众所周知的。mapreduce的大数据计算方式是在datanode本地使用map对数据进行一次计算。通常计算结果会比原始数据小很多。然后通过网络传输汇总后做reduce计算。这样就节省了大量的网络传输数据的时间浪费和资源消耗。Elasticsearch现在支持在每个数据节点上部署spark。在每个数据节点上由spark计算。不用查询所有数据,而是利用网络传输到spark集群进行计算。这种数据库和计算集群的混合部署是高性能的关键。类似的还有storm和kafka的关系。网络数据局部性的另一个长期存在的问题是分布式大数据下的多表连接问题。如果只是查询一个分布式表,那么用mapreduce表达计算问题不大。但是如果需要同时查询两个表,就意味着这两个表在物理上可能分布不均。最简单的策略之一是找到两个表中最小的一个,然后将表的内容广播到每个节点,然后加入。比较复杂的是对两个单表做mapreduce,然后根据同一个key把部分计算的结果汇集起来。第三种策略是保证数据的分布,让查询两张表时需要用到的数据一直在一起。没有完美的解决方案,也不可能有完美的解决方案。除非有一天网络带宽可以大到可以忽略不计。机器多了没啥好说的。两倍的机器有两倍的CPU,同时可以计算更多的数据。两倍的机器有两倍的磁头,可以同时扫描更多的字节。许多大数据框架的故事都是关于如何通过横向扩展解决最大的问题。但值得注意的是,集群可以最大,数据可以最多,但口袋里的钱不会是最多的。堆机器解决问题比升级主机便宜,但堆机器太多也很费钱。尤其是Hive,从一开始就是一个分布式的多机检索方案,一开始效率不是很高。堆机是乘法器。当数据库本身性能不高时,乘数起不到决定性作用。更高效的计算和计算检索过程不仅仅是磁盘扫描,它还包括一个可简单可复杂的转换过程。使用hyperloglog、countmin-sketch等有损算法可以大大提高统计计算的性能。数据库的join也是经常有算法创新的地方。计算实现就是算法是用C++、java还是python实现的。在java中是用bigInteger还是smallint实现的?不同的语言实现有一些固定的开销。不是说很快就需要C++,而是用python写一个for循环显然是没希望了。任何一个数据检索环节,只要包含python/ruby等语言的一对一for循环,肯定不会很快。结语希望这四点能够被记住,成为优化数据检索效率的指导性思维框架。无论您是设计mysql表结构还是优化sparksql应用程序。从这四个角度去思考,哪些环节是在受阻,手边的工具可以调整哪些参数,让随机读变成顺序读,如何设计表结构,尽量减少读取的数据量。为此,您必须非常非常了解该工具的底层实现。而不是盲目相信xx数据库是最好的数据库,所以一定要快什么的。不了解你的数据库,不了解你的计算引擎,快的时候不知道为什么快,慢的时候更不能优化。原文链接:segmentfault.com/a/1190000002884077
