【MySQL】分页优化

前段时间因为项目的缘由,对一个因为分页而形成性能较差的SQL进行优化,如今将优化过程当中学习到关于分页优化的知识跟你们简单分享下。html

分页不外乎limit,offset,在这两个关键字中,limit其实不是性能瓶颈的主要缘由,若是sql中定义了比较大的limit,说明了确实有一次性取出较多数据的需求,若是不是,就须要考虑limit参数是否须要调整了。这篇文章主要以offset为优化方向,介绍高offset下的性能优化手段。业界主要使用的仍是Innodb引擎,本文中的分析和方案主要针对Innodb引擎,其余存储未必适用。sql

查询执行原理

在开始介绍以前,咱们首先写一个最简单的分页sql,而且简单介绍下查询的数据读取原理,以便后面具体解决方案中进行对比。数据库

Query1: select * from table order by id asc limit 1000 offset 9000;缓存

对于这个查询,咱们首先来看看它的执行过程。性能优化

上图是一个Innodb聚簇索引的图示。因为咱们的查询没有使用where子句,并且要求返回全部列,因此查询会到聚簇索引中查询。查询在数据库中的扫描过程为:网络

查询首先会定位到第一条合乎条件的数据,也就是(15,34,Bob)这一行。 而后,根据这个叶子节点的首部信息,能够得知这个叶子节点中有多少行的数据。 若是这个叶子节点的数据达到offset的要求,读出数据直至达到limit要求。 若这个叶子节点没法完成查询,则经过指向兄弟节点的指针,根据排序要求,往下一个兄弟节点继续查询,直到知足limit与offset要求为止。数据结构

缓存子查询

咱们能够想象一下大多数用户翻页的习惯,通常来讲,用户都会一页一页地翻。利用用户的这一习惯,咱们能够将上一页的排序的最大/小值进行缓存,而后以此值做为查询传递到下一查询中。 仍是以上面的那条sql为例,咱们能够把那条sql拆成相赞成义的父子查询。咱们来看两条查询:性能

Query2: select * from table order by id limit 1000 offset 8000;学习

Query3: select * from table where id > (select id from test order by id limit 1 offset 8999) order by id limit 1000;测试

上面两条sql,2查询了8001~9000行,3查询了9001~10000行,3中的子查询返回了第9000行的id值。3不管意义仍是执行过程都与1相同,惟一不一样的,就是它是继承于2的。咱们能够看到,它是以2的结果数据中,id最大的那条数据为基础进行查询的。也就是说,在上一页查询后,咱们能够将第9000条数据的id进行缓存,当须要查询下一页数据时,咱们就能够直接将id替换3的子查询。这样遍历就不须要从第1个节点开始,节省了接近9000次的遍历。 注意与缺陷:

  • 须要肯定sql查询时的数据顺序:数据库对表及索引的维护默认采用升序维护,但在查询使用不一样的select对象时,选择的索引是不肯定的,因此建议在sql中加上order by子句。
  • 数据须要相对静态:若是数据在排序字段上变更频繁,如上例中取出第9000条数据的id进行缓存,可是在进行下一页查询以前,前面任意一条数据被删除,缓存的id就会失效。

覆盖索引法

在查询时,使用聚簇索引进行查询,一定会进行大量的IO。这是,能够考虑使用覆盖索引,将查询IO限定在索引中。在查询时,有时咱们实际须要的并非整行的数据,而只是其中的某几个字段,而当咱们select的数据列从索引中已经能够取出,数据库就不会对总表进行查询。 好比如今须要取出资产排名第1000用户的user_id,咱们只须要对assets与user_id创建联合索引,查询sql为:

Query4: select user_id from table order by assets desc limit 1 offset 999;

因为查询的where条件与返回列都在同一个索引能够知足筛选和返回的要求,查询不会再到总表中进行大量IO。 这里简单介绍一下覆盖索引优化的原理:

上图是一个Innodb的二级索引存储结构图。图中能够看到,二级索引的叶子节点存储的是最后一级的索引值、id、以及一个指向兄弟节点的指针。咱们假设一个块存储一个叶子节点。对于sql④,须要扫描1000个user_id,若是在总表中扫描,因为每行数据占用空间大,假设一个块中能存100行,读取一个块只能扫描100个user_id,须要读取10个块才能完成;但若是将查询限定在索引中,对于上述叶子节点中的数据结构,假设一个块能存储5000个索引值,只须要读取一个块就能完成咱们须要的offset 1000了。 若select的列是一个较大的数据列,如一段文本,或须要对全行数据取出时,能够经过子查询先将id查询出来,而后使用id到总表中查询。这是由于在二级索引叶子节点存储了id值,因此id就是一个天生的覆盖索引列。如上例中若是须要将全行数据取出,而不单是user_id一列,查询能够改成(深度学习能够参考:http://www.cnblogs.com/zhiqian-ali/p/4916064.html):

$ = select id from table order by assets desc limit 1 offset 999;
select * from table where id = $;
or
select * from table a,( select id from table order by assets desc limit 1 offset 999 ) b where a.id=b.id;

反向查找法

在使用offset进行查询时,参数值的大小对查询性能的影响很是大:当offset参数较小时,查询的性能很是高;但当offset的值逐渐增加,查询的耗时开始变得不可控制,须要一个方法将高offset查询的性能进行控制。优化的方法其实很简单:在一个1000行的表中,顺着数第1000条的数据,也正是倒着数第1条的数据。咱们只须要将数据进行倒序遍历,就能够将本来线性增加的查询耗时,转变为一个中间高、两头低的性能曲线了。如上述查询可优化为:

$ = select count(*) from table;
select * from table order by assets desc limit 1 offset ($ - 999);

在测试反向查找法的过程当中,发现耗时最高的并非在50%的位置,而是在60%的位置。这是由于在按照索引排序的顺序进行遍历时,索引的排布顺序与磁盘读取一致。当索引比较紧凑地存储在连续的几个块时,因为磁盘预读,在一次IO中多个有效块会被读出,而反向遍历时则没法享受磁盘预读带来的IO优化。 磁盘预读的优点依赖于索引的连续排布。当索引频繁移动时,数据的连续排布没法获得保证。在决定反向遍历临界值时,须要考虑数据索引值变更的频率的影响。若重建索引的是容许的,能够按期进行索引重建,使得索引紧密排布。

反向查找法存在一个比较致命的缺陷,就是须要对表进行count的操做:要想肯定反向offset参数,必须先得到总数量。对于行数量比较稳定的表,能够直接使用定时刷新的缓存值;对于不须要进行事务操做的表,能够考虑采用MyISAM引擎;而对于其余状况,目前还没找到比较好的解决方案。

总结

上述的几个方法各有优劣,没有办法选出一个解决分页性能瓶颈的万金油,须要结合实际应用场景进行一个或多个方案选用。

  • 缓存子查询的方法适用于顺序翻页的场景,但要求数据在指定排序上的序号是稳定的,才能保证缓存值有效。
  • 覆盖索引是一个适用性比较强的方法,与经常使用的利用索引优化查询性能的方案同样,它的缺点在于表修改时的性能降低,并且若是索引列的数据须要频繁更新,会致使索引排布不整齐,查询性能波动。
  • 反向查找法能够对偏移量较大的查询进行优化,但须要进行较高耗时的count查询,对于count查询的优化,目前只想到缓存与使用MyISAM引擎的办法。

上述知识大部分经过网络的资料搜集,结合实验测试进行总结,并无实际考察Mysql中的代码实现,如存在错误请帮忙订正。

相关文章
相关标签/搜索