作数据仓库的头两年,使用高配置单机 + MySQL的方式来实现全部的计算(包括数据的ETL,以及报表计算。没有OLAP)。用过MySQL自带的MYISAM和列存储引擎Infobright。这篇文章总结了本身和团队在那段时间碰到的一些常见性能问题和解决方案。 html
P.S.若是没有特别指出,下面说的mysql都是指用MYISAM作存储引擎。 java
业务需求中每每有计算一周/一个月的某某数据,好比计算最近一周某个特定页面的PV/UV。这里出现的问题就是实现的时候直接取整周的日志数据,而后进行计算。这样其实就出现了重复计算,某一天的数据在不一样的日子里被重复计算了7次。 mysql
解决办法很是之简单,就是把计算进行切分,若是是算PV,作法就是天天算好当天的PV,那么一周的PV就把算好的7天的PV相加。若是是算UV,那么天天从日志数据取出相应的访客数据,把最近七天的访客数据单独保存在一个表里面,计算周UV的时候直接用这个表作计算,而不须要从原始日志数据中抓上一大把数据来算了。 git
这是一个很是简单的问题,甚至不须要多少SQL的知识,可是在开发过程当中每每被视而不见。这就是只实现业务而忽略性能的表现。从小规模数据仓库作起的工程师,若是缺少这方面的意识和作事规范,就容易出现这种问题,等到数据仓库的数据量变得比较大的时候,才会发现。需求决定能力。 github
case when这个关键字,在作聚合的时候,能够很方便的将一份数据在一个SQL语句中进行分类的统计。举个例子,好比下面有一张成绩表(表名定为scores): 算法
如今须要统计小张的平均成绩,小明的平均成绩和小明的语文成绩。也就是最终结果应该是: sql
SQL实现以下: 数据库
若是如今这个成绩表有1200万条数据,包含了400万的名字 * 3个科目,上面的计算须要多长时间?我作了一个简单的测试,答案是5.5秒。 编程
而若是咱们在name列上面加了索引,而且把sql改为下面的写法: json
这样的话,只须要0.05秒就能完成。
那么若是有索引的话,前面的一种实现方法会不会变快?答案是不会,时间仍是跟原来同样。
而若是没有索引,后面一种写法会用多少时间?测试结果是3.3秒。
把几种状况再理一遍:
之因此后面一种写法老是比前面一种写法快,不一样之处就在因而否先在where里面把数据过滤掉。用where有两个好处:一个是有索引的话就能使用,而case when颇有可能用不到索引(关于索引的具体使用这里就不详细解释了,至少在这个例子中前一种写法没有用到索引),第二是可以提早过滤数据,哪怕没有索引,前一种写法扫描了三遍全表的数据(作一个case when扫一遍),后面的写法扫描一遍全表,把数据过滤了以后,case when就不用过这么多数据量了。
而实际状况是,开发常常只是为了实现功能逻辑,而习惯了在case when中限制条件取数据。这样在出现相似例子中的需求时,没有把应该限制的条件写到where里面。这是在实际代码中发现最多的一类问题。
编者注:
关于文中原做者认为的:前一种写法扫描了三遍全表的数据(作一个case when扫一遍)
我的存疑,有待考证,理论上执行计划不会这样弱:
explain select SQL_NO_CACHE avg (case when cate1 ='二手市场' then pv end) as es, avg (case when cate1 ='宠物' then pv end) as cw, avg (case when cate1 ='房产信息' then pv end) as fc from pagetype_lite_201408 where recDate >= '2014-08-01' and recDate <= '2014-08-07'; explain select SQL_NO_CACHE avg (case when cate1 ='二手市场' then pv end) as fc, avg (case when cate1 ='宠物' then pv end) as es, avg (case when cate1 ='房产信息' then pv end) as zp from pagetype_lite_201408 where recDate >= '2014-08-01' and recDate <= '2014-08-07' and cate1 in('二手市场','宠物','房产信息');
1 SIMPLE t_lj_pagetype_lite_201408 ALL Index_recDate_cate,Index_recDate_city 3438530 Using where能够看到,两个 SQL 的执行计划同样,并无出现一个三次扫描,一个一次。
在数据仓库中有一个重要的基础步骤,就是对数据进行清洗。好比数据源的数据若是以JSON方式存储,在mysql的数据仓库就必须将json中须要的字段提取出来,作成单独的表字段。这个步骤用sql直接处理很麻烦,因此能够用主流编程语言(好比java)的json库进行解析。解析的时候须要读取数据,一次性读取进来是不可能的,因此要分批读取(至关于分页了)。
最初的实现方式就是标记住每次取数据的偏移量,而后一批批读取:
这样的代码,在开始几句sql的时候执行速度还行,可是到后面会愈来愈慢,由于每次要读取大量数据再丢弃,实际上是一种浪费。
高效的实现方式,能够是用表中的主键进行分页。若是数据是按照主键排序的,那么能够是这样(这么作是要求主键的取值序列是连续的。假设主键的取值序列咱们比较清楚,是从10001-1000000的连续值):
就算数据不是按主键排序的,也能够经过限制主键的范围来分页。这样处理的话,主键的取值序列不连续也没有太大问题,就是每次拿到的数据会比理想中的少一些,反正是用在数据处理,不影响正确性:
这样的话,因为主键上面有索引,取数据速度就不会受到数据的具体位置的影响了。
索引的使用是关系数据库的SQL优化中一个很是重要的主题,也是一个常识性的东西。可是工程师在实际开发中每每是加完索引就以为万事大吉了,也不去检查索引是否被正确的使用了,因此仍是简单的提一下关于索引的案例。
仍是举例说明。假若有一个电商网站,积累了某一天的访问日志表item_visits,每条记录表示某一个商品(item)被访问了一次,包括访问者的一些信息,好比用户的id,昵称等等,有1200多万条数据。示例以下:
商品自己有一个商品表items,包含800多种商品,表名了商品名字和所属种类:
如今要计算每一个商品种类(item_type)被访问的次数。sql的实现不难:
而后既然是join,那么在join key上须要加索引。这时候有的工程师就随手在items的item_id上面加了索引。跑了一下,须要95秒。(p.s.在个人测试场景中,这个日志表有20多个字段,因此虽然这个表的记录数跟问题2中的那个表的记录数差很少,可是大小会差不少,了解这个背景能够解释这里的计算用时为何会远远超过问题2中的用时。)
前面说是随手加的索引,其实就已经在暗示加的有问题。那咱们在item_visit的item_id上面再加个索引,须要跑多久?80秒。
用explain查一下执行计划:
注意到这里是以日志表做为驱动表的(即从日志表开始扫描数据,而商品表是nest loop的内层嵌套),这样的话两个表的item_id都用到了,商品表的索引作join,日志表的索引能够作覆盖索引(这个覆盖索引就是比前面快的缘由)。看上去挺“划算”的,实际上因为放弃了item小表驱动,速度反而慢了不少。
接下来用straight_join的链接方式把这个sql强制改为小表驱动:
再来看执行计划:
虽然这样一来商品表的索引就用不到了,可是这实际上是正确的作法(固然若是条件容许,也未必要用straight join,把商品表上的索引去掉实际上是最合理的作法,这样mysql就会本身选择正确的执行计划了。),测试下来只须要8秒。缘由就在于大表驱动时,根据标准的Block Nested Loop Join算法,小表的数据会被反复循环读取。固然实际上小表是能够进cache而不用重复读取的,可是因为mysql只认索引有没有用上,因此仍是会反复读取小表(这个问题在这个slides的35页也有描述)。而若是小表驱动,就不会有这个问题。
后续更新:严格来讲,这个场景有一个限制条件,就是大表中的商品item_id只占所有item_id的一部分。若是大表中的商品item_id几乎均匀覆盖全部item_id,那么不管join时用哪一个表的索引,其实运行时间都差很少。原来作实验的时候忽视了这一点,后来从新尝试的时候发现了这个问题。特此补充。
小结一下:这里说了两个问题,一个是添加索引的时候须要想一想如何去加,在不是很确定的时候能够看看执行计划,而不是教条式的知道“join要加索引”。学习sql优化切忌只是背几个tips。另外就是mysql在选择执行计划的时候也不必定可以作到最好,若是发现mysql的执行计划有很大问题,那么就须要工程师进行调整,mysql中同样有相似oracle中的hint帮助咱们达到想要的目的,就像例子中的straight_join。
最后还须要注意的是覆盖索引和强制索引的问题。
在mysql中,须要join的表若是太多,会对性能形成很显著的降低。一样,举个例子来讲明。
首先生成一个表(命名为test),这个表只有60条记录,6个字段,其中第一个字段为主键:
而后作一个查询:
也就是说让test表跟本身关联。计算的结果显然是60,并且几乎不费时间。
可是若是是这样的查询(十个test表关联),会花费多少时间?
答案是:确定超过5分钟。由于作了实际测试,5分钟尚未出结果。这里的测试为了方便起见,用了一个表本身关联10次,实际上若是是不一样的表,效果也是同样的。
那么mysql到底在干什么呢?用show processlist去看一下运行时状况:
原来是处在statistics的状态。这个状态,根据mysql的解释是在根据统计信息去生成执行计划,固然这个解释确定是没有追根溯源。实际上mysql在生成执行计划的时候,其中有一个步骤,是肯定表的join顺序。默认状况下,mysql会把全部join顺序所有排列出来,依次计算各个join顺序的执行代价而且取最优的那个。这样一来,n个表join会有n!种状况。十个表join就是10!,大概300万,因此难怪mysql要分析半天了。
而在实际开发过程当中,曾经出现过30多个表关联的状况(有10^32种join顺序)。一旦出现,花费在statistics状态的时间每每是在1个小时以上。这还只是在表数据量都很是小,须要作顺序分析的点比较少的状况下。至于出现这种状况的缘由,无外乎咱们须要计算的汇总报表的字段太多,须要从各类各样的地方计算出来数据,而后再把数据拼接起来,报表在维护过程当中不断添加字段,又因为种种缘由没有去掉已经废弃的字段,这样字段一定会越来愈多,实现这些字段计算就须要用更多的临时计算结果表去关联到一块儿,结果须要关联的表也愈来愈多,成了mysql没法承受之重。
这个问题的解决方法有两个。从开发角度来讲,能够控制join的表个数。若是须要join的表太多,能够根据业务上的分类,先作一轮join,把表的数量控制在必定范围内,而后拿到第一轮的join结果,再作第二轮全局join,这样就不会有问题了。从运维角度来讲,能够设置optimizer_search_depth这个参数。它可以控制join顺序遍历的深度,进行贪婪搜索获得局部最优的顺序。通常有好多个表join的状况,都是上面说的相同维度的数据须要拼接成一张大表,对于join顺序基本上没什么要求。因此适当的把这个值调低,对于性能应该说没有影响。
Infobright是基于mysql的存储引擎,具备列存储/列压缩和知识网格等特性,比较适合数据仓库的计算。使用起来也不须要考虑索引之类的问题,很是方便。不过通过一段时间的运用,也发现了个别须要注意的问题。
一个问题和MYISAM相似,不要取不须要的数据。这里说的不须要的数据,包括不须要的列(Infobright的使用常识。固然行存储也要注意,只不过影响相对比较小,因此没有专门提到),和不须要的行(行数是能够扩展的,行存储一行基本上都能存在一个存储单元中,可是列存储一列明显不可能存在一个存储单元中)。
第二个问题,就是Infobright在长字符检索的时候并不给力。通常来讲,网站的访问日志中会有URL字段用来标识访问的具体地址。这样就有查找特定URL的需求。好比我要在cnblog的访问日志中查找到个人blog的访问次数:
相似这样在一个长字符串里面检索子串的需求,Infobright的执行时间测试下来是mysql的1.5-3倍。
至于速度慢的缘由,这里给出一个简要的解释:Infobright做为列式数据库使用了列存储的经常使用特性,就是压缩(列式数据库的压缩率通常要能作到10%之内,Infobright也不例外)。另外为了加快查找速度,它还使用了一种叫知识网格检索方式,通常状况下可以极大的减小须要读取的数据量。关于知识网格的原理已经超出了本篇文章的讨论篇幅,能够看这里了解。可是在查询url的时候,知识网格的优势没法体现出来,可是使用知识网格自己带来的检索代价和解压长字符串的代价却仍然存在,甚至比查询通常的数字类字段要来的大。
而后根据其原理能够给出一个可以说明问题的解决方法(虽然实用度不算高):若是整个表里面就有一个长字符串字段查询起来比较麻烦,能够把数据根据这个字段排序后再导入。这样一来按照该字段查询时,经过知识网格就可以屏蔽掉比较多的“数据包”(Infobright的数据压缩单元),而未排序的状况下符合条件的数据散布在各个“数据包”中,其解压工做量就大得多了。使用这个方法进行查询,测试下来其执行时间就只有mysql的0.5倍左右了。
[1] 数据仓库中的sql性能优化(MySQL篇)