程序员收藏必看系列:深度解析MySQL优化(一)javascript
下面会从3个不一样方面给出一些优化建议。但请等等,还有一句忠告要先送给你:不要听信你看到的关于优化的“绝对真理”,包括本文所讨论的内容,而应该是在实际的业务场景下经过测试来验证你关于执行计划以及响应时间的假设。css
这里总结几个可能容易理解错误的技巧:html
接下来将向你展现一系列建立高性能索引的策略,以及每条策略其背后的工做原理。但在此以前,先了解与索引相关的一些算法和数据结构,将有助于更好的理解后文的内容。java
一般咱们所说的索引是指B-Tree索引,它是目前关系型数据库中查找数据最为经常使用和有效的索引,大多数存储引擎都支持这种索引。使用B-Tree这个术语,是由于MySQL在CREATE TABLE或其它语句中使用了这个关键字,但实际上不一样的存储引擎可能使用不一样的数据结构,好比InnoDB就是使用的B+Tree。python
B+Tree中的B是指balance,意为平衡。须要注意的是,B+树索引并不能找到一个给定键值的具体行,它找到的只是被查找数据行所在的页,接着数据库会把页读入到内存,再在内存中进行查找,最后获得要查找的数据。程序员
在介绍B+Tree前,先了解一下二叉查找树,它是一种经典的数据结构,其左子树的值老是小于根的值,右子树的值老是大于根的值,以下图①。若是要在这课树中查找值为5的记录,其大体流程:先找到根,其值为6,大于5,因此查找左子树,找到3,而5大于3,接着找3的右子树,总共找了3次。一样的方法,若是查找值为8的记录,也须要查找3次。因此二叉查找树的平均查找次数为(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次,而顺序查找的话,查找值为2的记录,仅须要1次,但查找值为8的记录则须要6次,因此顺序查找的平均查找次数为:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,所以大多数状况下二叉查找树的平均查找速度比顺序查找要快。redis
因为二叉查找树能够任意构造,一样的值,能够构造出如图②的二叉查找树,显然这棵二叉树的查询效率和顺序查找差很少。若想二叉查找数的查询性能最高,须要这棵二叉查找树是平衡的,也即平衡二叉树(AVL树)。算法
平衡二叉树首先须要符合二叉查找树的定义,其次必须知足任何节点的两个子树的高度差不能大于1。显然图②不知足平衡二叉树的定义,而图①是一课平衡二叉树。平衡二叉树的查找性能是比较高的(性能最好的是最优二叉树),查询性能越好,维护的成本就越大。好比图①的平衡二叉树,当用户须要插入一个新的值9的节点时,就须要作出以下变更。数据库
经过一次左旋操做就将插入后的树从新变为平衡二叉树是最简单的状况了,实际应用场景中可能须要旋转屡次。至此咱们能够考虑一个问题,平衡二叉树的查找效率还不错,实现也很是简单,相应的维护成本还能接受,为何MySQL索引不直接使用平衡二叉树?缓存
随着数据库中数据的增长,索引自己大小随之增长,不可能所有存储在内存中,所以索引每每以索引文件的形式存储的磁盘上。这样的话,索引查找过程当中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级。能够想象一下一棵几百万节点的二叉树的深度是多少?若是将这么大深度的一颗二叉树放磁盘上,每读取一个节点,须要一次磁盘的I/O读取,整个查找的耗时显然是不可以接受的。那么如何减小查找过程当中的I/O存取次数?
一种行之有效的解决方法是减小树的深度,将二叉树变为m叉树(多路搜索树),而B+Tree就是一种多路搜索树。理解B+Tree时,只须要理解其最重要的两个特征便可:第一,全部的关键字(能够理解为数据)都存储在叶子节点(Leaf Page),非叶子节点(Index Page)并不存储真正的数据,全部记录节点都是按键值大小顺序存放在同一层叶子节点上。其次,全部的叶子节点由指针链接。以下图为高度为2的简化了的B+Tree。
怎么理解这两个特征?MySQL将每一个节点的大小设置为一个页的整数倍(缘由下文会介绍),也就是在节点空间大小必定的状况下,每一个节点能够存储更多的内结点,这样每一个结点能索引的范围更大更精确。全部的叶子节点使用指针连接的好处是能够进行区间访问,好比上图中,若是查找大于20而小于30的记录,只须要找到节点20,就能够遍历指针依次找到2五、30。若是没有连接指针的话,就没法进行区间查找。这也是MySQL使用B+Tree做为索引存储结构的重要缘由。
MySQL为什么将节点大小设置为页的整数倍,这就须要理解磁盘的存储原理。磁盘自己存取就比主存慢不少,在加上机械运动损耗(特别是普通的机械硬盘),磁盘的存取速度每每是主存的几百万分之一,为了尽可能减小磁盘I/O,磁盘每每不是严格按需读取,而是每次都会预读,即便只须要一个字节,磁盘也会从这个位置开始,顺序向后读取必定长度的数据放入内存,预读的长度通常为页的整数倍。
页是计算机管理存储器的逻辑块,硬件及OS每每将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块称为一页(许多OS中,页的大小一般为4K)。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,而后一块儿返回,程序继续运行。
MySQL巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每一个节点只须要一次I/O就能够彻底载入。为了达到这个目的,每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了读取一个节点只需一次I/O。假设B+Tree的高度为h,一次检索最多须要h-1次I/O(根节点常驻内存),复杂度O(h) = O(logmN)。实际应用场景中,M一般较大,经常超过100,所以树的高度通常都比较小,一般不超过3。
最后简单了解下B+Tree节点的操做,在总体上对索引的维护有一个大概的了解,虽然索引能够大大提升查询效率,但维护索引仍要花费很大的代价,所以合理的建立索引也就尤其重要。
仍以上面的树为例,咱们假设每一个节点只能存储4个内节点。首先要插入第一个节点28,以下图所示。
leaf page和index page都没有满
接着插入下一个节点70,在Index Page中查询后得知应该插入到50 - 70之间的叶子节点,但叶子节点已满,这时候就须要进行也分裂的操做,当前的叶子节点起点为50,因此根据中间值来拆分叶子节点,以下图所示。
Leaf Page拆分
最后插入一个节点95,这时候Index Page和Leaf Page都满了,就须要作两次拆分,以下图所示。
Leaf Page与Index Page拆分
拆分后最终造成了这样一颗树。
最终树
B+Tree为了保持平衡,对于新插入的值须要作大量的拆分页操做,而页的拆分须要I/O操做,为了尽量的减小页的拆分操做,B+Tree也提供了相似于平衡二叉树的旋转功能。当Leaf Page已满但其左右兄弟节点没有满的状况下,B+Tree并不急于去作拆分操做,而是将记录移到当前所在页的兄弟节点上。一般状况下,左兄弟会被先检查用来作旋转操做。就好比上面第二个示例,当插入70的时候,并不会去作页拆分,而是左旋操做。
左旋操做
经过旋转操做能够最大限度的减小页分裂,从而减小索引维护过程当中的磁盘的I/O操做,也提升索引维护效率。须要注意的是,删除节点跟插入节点相似,仍然须要旋转和拆分操做,这里就再也不说明。
CREATE TABLE People(
last_name varchar(50) not null, first_name varchar(50) not null, dob date not null, gender enum(`m`,`f`) not null, key(last_name,first_name,dob) );
对于表中每一行数据,索引中包含了last_name、first_name、dob列的值,下图展现了索引是如何组织数据存储的。
索引如何组织数据存储,来自:高性能MySQL
能够看到,索引首先根据第一个字段来排列顺序,当名字相同时,则根据第三个字段,即出生日期来排序,正是由于这个缘由,才有了索引的“最左原则”。
MySQL不会使用索引的状况:非独立的列
“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。好比:
select * from where id + 1 = 5
咱们很容易看出其等价于 id = 4,可是MySQL没法自动解析这个表达式,使用函数是一样的道理。
前缀索引
若是列很长,一般能够索引开始的部分字符,这样能够有效节约索引空间,从而提升索引效率。
多列索引和索引顺序
在多数状况下,在多个列上创建独立的索引并不能提升查询性能。理由很是简单,MySQL不知道选择哪一个索引的查询效率更好,因此在老版本,好比MySQL5.0以前就会随便选择一个列的索引,而新的版本会采用合并索引的策略。举个简单的例子,在一张电影演员表中,在actor_id和film_id两个列上都创建了独立的索引,而后有以下查询:
select film_id,actor_id from film_actor where actor_id = 1 or film_id = 1
老版本的MySQL会随机选择一个索引,但新版本作以下的优化:
select film_id,actor_id from film_actor where actor_id = 1 union all select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1
当出现多个索引作相交操做时(多个AND条件),一般来讲一个包含全部相关列的索引要优于多个独立索引。
当出现多个索引作联合操做时(多个OR条件),对结果集的合并、排序等操做须要耗费大量的CPU和内存资源,特别是当其中的某些索引的选择性不高,须要返回合并大量数据时,查询成本更高。因此这种状况下还不如走全表扫描。
所以explain时若是发现有索引合并(Extra字段出现Using union),应该好好检查一下查询和表结构是否是已是最优的,若是查询和表都没有问题,那只能说明索引建的很是糟糕,应当慎重考虑索引是否合适,有可能一个包含全部相关列的多列索引更适合。
前面咱们提到过索引如何组织数据存储的,从图中能够看到多列索引时,索引的顺序对于查询是相当重要的,很明显应该把选择性更高的字段放到索引的前面,这样经过第一个字段就能够过滤掉大多数不符合条件的数据。
索引选择性是指不重复的索引值和数据表的总记录数的比值,选择性越高查询效率越高,由于选择性越高的索引可让MySQL在查询时过滤掉更多的行。惟一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
理解索引选择性的概念后,就不难肯定哪一个字段的选择性较高了,查一下就知道了,好比:
SELECT * FROM payment where staff_id = 2 and customer_id = 584
是应该建立(staff_id,customer_id)的索引仍是应该颠倒一下顺序?执行下面的查询,哪一个字段的选择性更接近1就把哪一个字段索引前面就好。
select count(distinct staff_id)/count(*) as staff_id_selectivity, count(distinct customer_id)/count(*) as customer_id_selectivity, count(*) from payment
多数状况下使用这个原则没有任何问题,但仍然注意你的数据中是否存在一些特殊状况。举个简单的例子,好比要查询某个用户组下有过交易的用户信息:
select user_id from trade where user_group_id = 1 and trade_amount > 0
MySQL为这个查询选择了索引(user_group_id,trade_amount),若是不考虑特殊状况,这看起来没有任何问题,但实际状况是这张表的大多数数据都是从老系统中迁移过来的,因为新老系统的数据不兼容,因此就给老系统迁移过来的数据赋予了一个默认的用户组。这种状况下,经过索引扫描的行数跟全表扫描基本没什么区别,索引也就起不到任何做用。
推广开来讲,经验法则和推论在多数状况下是有用的,能够指导咱们开发和设计,但实际状况每每会更复杂,实际业务场景下的某些特殊状况可能会摧毁你的整个设计。
select user.* from user where login_time > '2017-04-01' and age between 18 and 30;
这个查询有一个问题:它有两个范围条件,login_time列和age列,MySQL可使用login_time列的索引或者age列的索引,但没法同时使用它们。
索引条目远小于数据行大小,若是只读取索引,极大减小数据访问量
索引是有按照列值顺序存储的,对于I/O密集型的范围查询要比随机从磁盘读取每一行数据的IO要少的多
扫描索引自己很快,由于只须要从一条索引记录移动到相邻的下一条记录。但若是索引自己不能覆盖全部须要查询的列,那么就不得不每扫描一条索引记录就回表查询一次对应的行。这个读取操做基本上是随机I/O,所以按照索引顺序读取数据的速度一般要比顺序地全表扫描要慢。
在设计索引时,若是一个索引既可以知足排序,又知足查询,是最好的。
只有当索引的列顺序和ORDER BY子句的顺序彻底一致,而且全部列的排序方向也同样时,才可以使用索引来对结果作排序。若是查询须要关联多张表,则只有ORDER BY子句引用的字段所有为第一张表时,才能使用索引作排序。ORDER BY子句和查询的限制是同样的,都要知足最左前缀的要求(有一种状况例外,就是最左的列被指定为常数,下面是一个简单的示例),其余状况下都须要执行排序操做,而没法利用索引排序。
// 最左列为常数,索引:(date,staff_id,customer_id) select staff_id,customer_id from demo where date = '2015-06-01' order by staff_id,customer_id
大多数状况下都应该尽可能扩展已有的索引而不是建立新索引。但有极少状况下出现性能方面的考虑须要冗余索引,好比扩展已有索引而致使其变得过大,从而影响到其余使用该索引的查询。
关于索引这个话题打算就此打住,最后要说一句,索引并不老是最好的工具,只有当索引帮助提升查询速度带来的好处大于其带来的额外工做时,索引才是有效的。对于很是小的表,简单的全表扫描更高效。对于中到大型的表,索引就很是有效。对于超大型的表,创建和维护索引的代价随之增加,这时候其余技术也许更有效,好比分区表。最后的最后,explain后再提测是一种美德。
COUNT()多是被你们误解最多的函数了,它有两种不一样的做用,其一是统计某个列值的数量,其二是统计行数。统计列值时,要求列值是非空的,它不会统计NULL。若是确认括号中的表达式不可能为空时,实际上就是在统计行数。最简单的就是当使用COUNT(*)时,并非咱们所想象的那样扩展成全部的列,实际上,它会忽略全部的列而直接统计行数。
咱们最多见的误解也就在这儿,在括号内指定了一列却但愿统计结果是行数,并且还经常误觉得前者的性能会更好。但实际并不是这样,若是要统计行数,直接使用COUNT(*),意义清晰,且性能更好。
有时候某些业务场景并不须要彻底精确的COUNT值,能够用近似值来代替,EXPLAIN出来的行数就是一个不错的近似值,并且执行EXPLAIN并不须要真正地去执行查询,因此成本很是低。一般来讲,执行COUNT()都须要扫描大量的行才能获取到精确的数据,所以很难优化,MySQL层面还能作得也就只有覆盖索引了。若是不还能解决问题,只有从架构层面解决了,好比添加汇总表,或者使用redis这样的外部缓存系统。
在大数据场景下,表与表之间经过一个冗余字段来关联,要比直接使用JOIN有更好的性能。若是确实须要使用关联查询的状况下,须要特别注意的是:
确保ON和USING字句中的列上有索引。在建立索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,若是优化器关联的顺序是A、B,那么就不须要在A表的对应列上建立索引。没有用到的索引会带来额外的负担,通常来讲,除非有其余理由,只须要在关联顺序中的第二张表的相应列上建立索引(具体缘由下文分析)。
确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化。
要理解优化关联查询的第一个技巧,就须要理解MySQL是如何执行关联查询的。当前MySQL关联执行的策略很是简单,它对任何的关联都执行嵌套循环关联操做,即先在一个表中循环取出单条数据,而后在嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到全部表中匹配的行为为止。而后根据各个表匹配的行,返回查询中须要的各个列。
太抽象了?以上面的示例来讲明,好比有这样的一个查询:
SELECT A.xx,B.yy FROM A INNER JOIN B USING(c) WHERE A.xx IN (5,6)
假设MySQL按照查询中的关联顺序A、B来进行关联操做,那么能够用下面的伪代码表示MySQL如何完成这个查询:
outer_iterator = SELECT A.xx,A.c FROM A WHERE A.xx IN (5,6); outer_row = outer_iterator.next; while(outer_row) { inner_iterator = SELECT B.yy FROM B WHERE B.c = outer_row.c; inner_row = inner_iterator.next; while(inner_row) { output[inner_row.yy,outer_row.xx]; inner_row = inner_iterator.next; } outer_row = outer_iterator.next; }
能够看到,最外层的查询是根据A.xx列来查询的,A.c上若是有索引的话,整个关联查询也不会使用。再看内层的查询,很明显B.c上若是有索引的话,可以加速查询,所以只须要在关联顺序中的第二张表的相应列上建立索引便可。
当须要分页操做时,一般会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY字句。若是有对应的索引,一般效率会不错,不然,MySQL须要作大量的文件排序操做。
一个常见的问题是当偏移量很是大的时候,好比:LIMIT 10000 20这样的查询,MySQL须要查询10020条记录而后只返回20条记录,前面的10000条都将被抛弃,这样的代价很是高。
优化这种查询一个最简单的办法就是尽量的使用覆盖索引扫描,而不是查询全部的列。而后根据须要作一次关联查询再返回全部的列。对于偏移量很大时,这样作的效率会提高很是大。考虑下面的查询:
SELECT film_id,description FROM film ORDER BY title LIMIT 50,5;
若是这张表很是大,那么这个查询最好改为下面的样子:
SELECT film.film_id,film.description FROM film INNER JOIN ( SELECT film_id FROM film ORDER BY title LIMIT 50,5 ) AS tmp USING(film_id);
这里的延迟关联将大大提高查询效率,让MySQL扫描尽量少的页面,获取须要访问的记录后在根据关联列回原表查询所须要的列。
有时候若是可使用书签记录上次取数据的位置,那么下次就能够直接从该书签记录的位置开始扫描,这样就能够避免使用OFFSET,好比下面的查询:
SELECT id FROM t LIMIT 10000, 10;
改成:
SELECT id FROM t WHERE id > 10000 LIMIT 10;
其余优化的办法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表中只包含主键列和须要作排序的列。
MySQL处理UNION的策略是先建立临时表,而后再把各个查询结果插入到临时表中,最后再来作查询。所以不少优化策略在UNION查询中都没有办法很好的时候。常常须要手动将WHERE、LIMIT、ORDER BY等字句“下推”到各个子查询中,以便优化器能够充分利用这些条件先优化。
除非确实须要服务器去重,不然就必定要使用UNION ALL,若是没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会致使整个临时表的数据作惟一性检查,这样作的代价很是高。固然即便使用ALL关键字,MySQL老是将结果放入临时表,而后再读出,再返回给客户端。虽然不少时候没有这个必要,好比有时候能够直接把每一个子查询的结果返回给客户端。
理解查询是如何执行以及时间都消耗在哪些地方,再加上一些优化过程的知识,能够帮助你们更好的理解MySQL,理解常见优化技巧背后的原理。但愿本文中的原理、示例可以帮助你们更好的将理论和实践联系起来,更多的将理论知识运用到实践中。
其余也没啥说的了,给你们留两个思考题吧,能够在脑壳里想一想答案,这也是你们常常挂在嘴边的,但不多有人会思考为何?
有很是多的程序员在分享时都会抛出这样一个观点:尽量不要使用存储过程,存储过程很是不容易维护,也会增长使用成本,应该把业务逻辑放到客户端。既然客户端都能干这些事,那为何还要存储过程?
(存储过程是通过编译的一系列SQL语句的集合,若是用程序来实现,可能须要屡次链接数据库,这样下降了程序和数据库的耦合。)
JOIN自己也挺方便的,直接查询就行了,为何还须要视图呢?(视图是一张虚拟表,简化了用户的操做,并且能够对机密数据提供安全保护。)