基本上只要是作后台开发,都会接触到分页这个需求或者功能吧。基本上你们都是会用MySQL的LIMIT来处理,并且我如今负责的项目也是这样写的。可是一旦数据量起来了,其实LIMIT的效率会极其的低,这一篇文章就来说一下LIMIT子句优化的。web
不少业务场景都须要用到分页这个功能,基本上都是用LIMIT来实现。算法
建表而且插入200万条数据:sql
# 新建一张t5表 CREATE TABLE `t5` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `text` varchar(100) NOT NULL, PRIMARY KEY (`id`), KEY `ix_name` (`name`), KEY `ix_test` (`text`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; # 建立存储过程插入200万数据 CREATE PROCEDURE t5_insert_200w() BEGIN DECLARE i INT; SET i=1000000; WHILE i<=3000000 DO INSERT INTO t5(`name`,text) VALUES('god-jiang666',concat('text', i)); SET i=i+1; END WHILE; END; # 调用存储过程插入200万数据 call t5_insert_200w();
在翻页比较少的状况下,LIMIT是不会出现任何性能上的问题的。数据库
可是若是用户须要查到最后面的页数呢?性能优化
一般状况下,咱们要保证全部的页面能够正常跳转,由于不会使用order by xxx desc这样的倒序SQL来查询后面的页数,而是采用正序顺序来作分页查询:svg
select * from t5 order by text limit 100000, 10;
采用这种SQL查询分页的话,从200万数据中取出这10行数据的代价是很是大的,须要先排序查出前1000010条记录,而后抛弃前面1000000条。个人macbook pro跑出来花了5.578秒。性能
接下来咱们来看一下,上面这条SQL语句的执行计划:优化
explain select * from t5 order by text limit 1000000, 10;
从执行计划能够看出,在大分页的状况下,MySQL没有走索引扫描,即便text字段我已经加上了索引。spa
这是为何呢?设计
回到MySQL索引(二)如何设计索引中有说起到,MySQL数据库的查询优化器是采用了基于代价的,而查询代价的估算是基于CPU代价和IO代价。
若是MySQL在查询代价估算中,认为全表扫描方式比走索引扫描的方式效率更高的话,就会放弃索引,直接全表扫描。
这就是为何在大分页的SQL查询中,明明给该字段加了索引,可是MySQL却走了全表扫描的缘由。
而后咱们继续用上面的查询SQL来验证个人猜测:
explain select * from t5 order by text limit 7774, 10;
explain select * from t5 order by text limit 7775, 10;
以上的实验均在个人mbp上运行的,在7774这个临界点上,MySQL分别采用了索引扫描和全表扫描的查询优化方式。
因此能够认为MySQL会根据它本身的代价查询优化器来判断是否使用索引。
因为MySQL的查询优化器的算法核心是咱们没法人工干预的,因此咱们的优化思路就要着手于如何让分页维持在最佳的的分页临界点。
若是一条SQL语句,经过索引能够直接获取查询的结果,再也不须要回表查询,就称这个索引为覆盖索引。
在MySQL数据库中使用explain关键字查看执行计划,若是extra这一列显示Using index,就表示这条SQL语句使用了覆盖索引。
让咱们来对比一下使用了覆盖索引,性能会提高多少吧。
# 没有使用覆盖索引 select * from t5 order by text limit 1000000, 10;
此次查询花了3.690秒,让咱们看一下使用了覆盖索引优化会提高多少性能吧。
# 使用了覆盖索引 select id, `text` from t5 order by text limit 1000000, 10;
从上面的对比中,超大分页查询中,使用了覆盖索引以后,花了0.201秒,而没有使用覆盖索引花了3.690秒,提升了18倍多,这在实际开发中,就是一个大的性能优化了。(该数据在个人mbp上运行得出)
由于实际开发中,用SELECT查询一两列操做是很是少的,所以上述的覆盖索引的适用范围就比较有限。
因此咱们能够经过把分页的SQL语句改写成子查询的方法得到性能上的提高。
select * from t5 where id>=(select id from t5 order by text limit 1000000, 1) limit 10;
其实使用这种方法,提高的效率和上面使用了覆盖索引基本一致。
可是这种优化方法也有局限性:
和上述的子查询作法相似,咱们可使用JOIN,先在索引列上完成分页操做,而后再回表获取所须要的列。
select a.* from t5 a inner join (select id from t5 order by text limit 1000000, 10) b on a.id=b.id;
从实验中能够得出,在采用JOIN改写后,上面的两个局限性都已经解除了,并且SQL的执行效率也没有损失。
和上面使用的方法都不一样,记录上次结束位置优化思路是使用某种变量记录上一次数据的位置,下次分页时直接从这个变量的位置开始扫描,从而避免MySQL扫描大量的数据再抛弃的操做。
select * from t5 where id>=1000000 limit 10;
根据以上实验,不可贵出,因为使用了主键索引作分页操做,SQL的性能是最快的。