MySQL优化案例系列-mysql分页优化
通常,我们会采用order by limit start, offset 的方式来进行分页查询。例如下面这个sql:
select * from `t1` where ftype=1 order by id desc limit 100, 10;
或者像下面这个不带任何条件的分页sql:
select * from `t1` order by id desc limit 100, 10;
一般而言,分页sql的耗时随着 start 值的增加而急剧增加,我们来看下面这2个不同起始值的分页sql执行耗时:
yejr@imysql.com> select * from `t1` where ftype=1 order by id desc limit 500, 10; … 10 rows in set (0.05 sec) yejr@imysql.com> select * from `t1` where ftype=6 order by id desc limit 935500, 10; … 10 rows in set (2.39 sec)
可以看到,随着分页数量的增加,sql查询耗时也有数十倍增加,显然不科学。今天我们就来分析下,如何能优化这个分页方案。 一般滴,想要优化分页的终极方案就是:没有分页,哈哈哈~~~,不要说我讲废话,确实如此,可以把分页算法交给sphinx、lucence等第三方解决方案,没必要让mysql来做它不擅长的事情。 当然了,有小伙伴说,用第三方太麻烦了,我们就想用mysql来做这个分页,咋办呢?莫急,且待我们慢慢分析,先看下表ddl、数据量、查询sql的执行计划等信息:
yejr@imysql.com> show create table `t1`; create table `t1` ( `id` int(10) unsigned not null auto_increment, ... `ftype` tinyint(3) unsigned not null, ... primary key (`id`) ) engine=innodb default charset=utf8; yejr@imysql.com> select count(*) from t1; +----------+ | count(*) | +----------+ | 994584 | +----------+ yejr@imysql.com> explain select * from `t1` where ftype=1 order by id desc limit 500, 10\g *************************** 1. row *************************** id: 1 select_type: simple table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 510 extra: using where yejr@imysql.com> explain select * from `t1` where ftype=1 order by id desc limit 935500, 10\g *************************** 1. row *************************** id: 1 select_type: simple table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 935510 extra: using where
可以看到,虽然通过主键索引进行扫描了,但第二个sql需要扫描的记录数太大了,而且需要先扫描约935510条记录,然后再根据排序结果取10条记录,这肯定是非常慢了。 针对这种情况,我们的优化思路就比较清晰了,有两点:
1、尽可能从索引中直接获取数据,避免或减少直接扫描行数据的频率
2、尽可能减少扫描的记录数,也就是先确定起始的范围,再往后取n条记录即可
据此,我们有两种相应的改写方法:子查询、表连接,即下面这样的:
#采用子查询的方式优化,在子查询里先从索引获取到最大id,然后倒序排,再取10行结果集
#注意这里采用了2次倒序排,因此在取limit的start值时,比原来的值加了10,即935510,否则结果将和原来的不一致
yejr@imysql.com> explain select * from (select * from `t1` where id > ( select id from `t1` where ftype=1 order by id desc limit 935510, 1) limit 10) t order by id desc\g *************************** 1. row *************************** id: 1 select_type: primary table: <derived2> type: all possible_keys: null key: null key_len: null ref: null rows: 10 extra: using filesort *************************** 2. row *************************** id: 2 select_type: derived table: t1 type: all possible_keys: primary key: null key_len: null ref: null rows: 973192 extra: using where *************************** 3. row *************************** id: 3 select_type: subquery table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 935511 extra: using where #采用inner join优化,join子句里也优先从索引获取id列表,然后直接关联查询获得最终结果,这里不需要加10 yejr@imysql.com> explain select * from `t1` inner join ( select id from `t1` where ftype=1 order by id desc limit 935500,10) t2 using (id)\g *************************** 1. row *************************** id: 1 select_type: primary table: <derived2> type: all possible_keys: null key: null key_len: null ref: null rows: 935510 extra: null *************************** 2. row *************************** id: 1 select_type: primary table: t1 type: eq_ref possible_keys: primary key: primary key_len: 4 ref: t2.id rows: 1 extra: null *************************** 3. row *************************** id: 2 select_type: derived table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 973192 extra: using where
然后我们来对比下这2个优化后的新sql执行时间:
yejr@imysql.com> select * from (select * from `t1` where id > ( select id from `t1` where ftype=1 order by id desc limit 935510, 1) limit 10) t order by id desc; ... rows in set (1.86 sec) #采用子查询优化,从profiling的结果来看,相比原来的那个sql快了:28.2% yejr@imysql.com> select * from `t1` inner join ( select id from `t1` where ftype=1 order by id desc limit 935500,10) t2 using (id); ... 10 rows in set (1.83 sec) #采用inner join优化,从profiling的结果来看,相比原来的那个sql快了:30.8%
我们再来看一个不带过滤条件的分页sql对比:
#原始sql yejr@imysql.com> explain select * from `t1` order by id desc limit 935500, 10\g *************************** 1. row *************************** id: 1 select_type: simple table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 935510 extra: null yejr@imysql.com> select * from `t1` order by id desc limit 935500, 10; ... 10 rows in set (2.22 sec) #采用子查询优化 yejr@imysql.com> explain select * from (select * from `t1` where id > ( select id from `t1` order by id desc limit 935510, 1) limit 10) t order by id desc; *************************** 1. row *************************** id: 1 select_type: primary table: <derived2> type: all possible_keys: null key: null key_len: null ref: null rows: 10 extra: using filesort *************************** 2. row *************************** id: 2 select_type: derived table: t1 type: all possible_keys: primary key: null key_len: null ref: null rows: 973192 extra: using where *************************** 3. row *************************** id: 3 select_type: subquery table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 935511 extra: using index yejr@imysql.com> select * from (select * from `t1` where id > ( select id from `t1` order by id desc limit 935510, 1) limit 10) t order by id desc; … 10 rows in set (2.01 sec) #采用子查询优化,从profiling的结果来看,相比原来的那个sql快了:10.6% #采用inner join优化 yejr@imysql.com> explain select * from `t1` inner join ( select id from `t1`order by id desc limit 935500,10) t2 using (id)\g *************************** 1. row *************************** id: 1 select_type: primary table: type: all possible_keys: null key: null key_len: null ref: null rows: 935510 extra: null *************************** 2. row *************************** id: 1 select_type: primary table: t1 type: eq_ref possible_keys: primary key: primary key_len: 4 ref: t1.id rows: 1 extra: null *************************** 3. row *************************** id: 2 select_type: derived table: t1 type: index possible_keys: null key: primary key_len: 4 ref: null rows: 973192 extra: using index yejr@imysql.com> select * from `t1` inner join ( select id from `t1`order by id desc limit 935500,10) t2 using (id); … 10 rows in set (1.70 sec) #采用inner join优化,从profiling的结果来看,相比原来的那个sql快了:30.2%
至此,我们看到采用子查询或者inner join进行优化后,都有大幅度的提升,这个方法也同样适用于较小的分页,虽然limit开始的 start 位置小了很多,sql执行时间也快了很多,但采用这种方法后,带where条件的分页分别能提高查询效率:24.9%、156.5%,不带where条件的分页分别提高查询效率:554.5%、11.7%,各位可以自行进行测试验证。单从提升比例说,还是挺可观的,确保这些优化方法可以适用于各种分页模式,就可以从一开始就是用。 我们来看下各种场景相应的提升比例是多少:
大分页,带where | 大分页,不带where | 大分页平均提升比例 | 小分页,带where | 小分页,不带where | 总体平均提升比例 | |
子查询优化 | 28.20% | 10.60% | 19.40% | 24.90% | 554.40% | 154.53% |
inner join优化 | 30.80% | 30.20% | 30.50% | 156.50% | 11.70% | 57.30% |
结论:这样看就和明显了,尤其是针对大分页的情况,因此我们优先推荐使用inner join方式优化分页算法。
上述每次测试都重启mysqld实例,并且加了sql_no_cache,以保证每次都是直接数据文件或索引文件中读取。如果数据经过预热后,查询效率会一定程度提升,但但上述相应的效率提升比例还是基本一致的。
2014/07/28后记更新:
其实如果是不带任何条件的分页,就没必要用这么麻烦的方法了,可以采用对主键采用范围检索的方法,例如参考这篇:advance for mysql pagination