欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2024-02-12 14:52:46
通常,我们会采用order by limit start, offset 的方式来进行分页查询。例如下面这个sql: select * from `t1` whe...

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