当前位置: 移动技术网 > IT编程>数据库>Mysql > MySQL优化案例系列-mysql分页优化

MySQL优化案例系列-mysql分页优化

2017年12月12日  | 移动技术网IT编程  | 我要评论

通常,我们会采用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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网