面试题必问-大表分页的优化

面试题必问-大表分页的优化

引子

先来看一段我们平时的分页代码

Request: http://127.0.0.1/article/list?page=1000&page_size=100   
mysql > select count(1) from article where tag_id=1;
mysql > select id,title,author,create_time from article where tag_id=1 order by create_time desc 

带入参数

mysql > select id,title,author,create_time from article where tag_id=1 order by create_time desc limit 100000,100;

分析一下这调sql语句存在的问题(下文所有分析均以Innodb引擎表为例)

  • order by create_time 按创建时间倒叙展示,这个需求很常见,一般分页都是伴随着排序的要求.如果create_time没有创建索引,那么SQL不可避免的要用到filesort,这意味着将进行全表扫描.表容量达到百万量级就很难一次性装入内存.
    也就是说,filesort操作不单要占用大量CPU进行排序计算,也会产生很多磁盘IO进行外部排序.
  • 排序完成后,要经过offset 100000条数据才能定位到我们需要的100条数据,虽然B+一个叶子页上可以存储多条记录,但是扫描大量的offset同样会引发较多IO,
    并且,这些IO操作很有可能是随机IO.

针对以上两个主要问题,如下解决思路.

  1. create_time上增加索引,避免排序,避免全表扫描.
  2. limit <$offset>,<$rows>改进为limit 0,<$rows>;
  3. 从需求上避免指定页数跳转,控制页的深度,隐藏展示数据总条数.redis里面做分页

来逐个详细分析一下上述方案.

Step.1 create_time上增加索引,避免排序,避免全表扫描.

一般来将,create_timeid的增长方向是一致的,所以一般我们不用对create_time列创建索引.
也就是说order by create_time desc等价于order by id desc.

但是在实际工作中,排序字段可是很多变的,比如约会站点更注重最新活跃时间,性别这些字段.

select id,name,age,sex,latest_active_time from user where country='beijing` and sex='w' and age='18_25' order by latest_active_time desc.  

如果开发人员在创建表时对社交心理学没有研究,那创建出来的索引就会偏离方向了.这个时候,没有现成的id,可以给我们利用了.怎么办呢?
没有困难我们就自己制造困难.

msyql > alter table user add index latest_active_time(latest_active_time)

虽然思路没错,但是有几个问题.

扫描索引是很快的,因为索引结构通常比较小,而且在内存中排列方式通常是紧凑的.但是如果索引不能覆盖查询所需要的全部列,那就不得不每扫描到一条匹配的
索引记录,就回表查询一次(单行回表)不是批量回表.这些IO都是随机IO.因此按照索引顺序读取数据的速度通常要比顺序的全表扫描要慢,尤其是在IO负载比较高时.(认真读十遍).
(解释:因为索引顺序与数据行的插入顺序极有可能不一致,而数据行的物理存储顺序通常是与插入顺序一致).

另外,即使我们根据latest_active_time得出了主键值(innodb的二级索引中叶子结点存储的是主键值而不是行数据的指针),我们仍然需要根据从二级索引得到得主键序列
对主键索引进行回表查询,取出行数据中的目标字段.

如果MySQL可以使用同一个索引,既满足排序,又能用于查找行,那该有多好.

Tips:这不就是在说覆盖索引吗

介绍另外一种方案: 延迟关联

select <cols> from user innser join (
                            select primary_key_col from user where x.sex='w' order by rating limit 100000,10
                        ) as x  using(primary_key_col); 

解析: 子查询利用覆盖索引原理查询,快速返回要查询的主键值(虽然也有offset,但是发生在索引结构中),然后根据主键值与原表进行关联获取需要的行.
这样处理的原理是减少了扫描丢弃行的开销.(select <row>在索引中就可以全部命中,不需要根据主键值额外回表一次).比较优雅的在子查询种完成了扫描,排序两个操作.

Tips: 利用索引的有序性尽量避免排序.(索引在创建时维护有序性),但是我们要知道,order by子句要满足最左前缀才要求,索引才可以生效.

我们要明确一点:无论如何创建索引,都无法彻底解决这个问题.反范式化,预计算,缓存是解决问题的仅有方案.

MySQL有两种方式可以生成有序的结果集

  • 通过filesort
  • 按索引顺序扫描

Tips:如果要按照反方向排序,我们可以存储列值的相反数.

Step.2 将limit <$offset>,<$rows>改进为limit 0,<$rows>;

这个实现起来比较简单,一般有两种场景

Request: http://127.0.0.1/article/list?page=1000&page_size=100   
mysql > select id,title,author,create_time from article ordery by id desc limit 100000,100;

如果你的id连续性比较好,可以直接通过数学计算得出offset后的起始id,转化为limit 100.

mysql > select id,title,author,create_time from article where id<100000 ordery by id desc limit 0,100;

id<100000一下子就可以排除掉100000行数据的扫描工作.但是这种应用场景存在的不多.SQL里面包含where时,数学公式很容易失效.

当页数比较深时,允许客户端将最后一条数据的ID传回到服务端,服务端先过滤,再排序.

Request: http://127.0.0.1/article/list?page=1000&page_size=100&last_id=100000   
mysql > select id,title,author,create_time from article where id<100000 ordery by id desc limit 0,100;

Step.3 从需求上避免

很多时候,用户并不需要总条数,指定页数跳转这些功能.参考百度搜索列表,只展示10页的跳转连接.

参考资料

  • 高性能mysql
  • https://www.cnblogs.com/starry-skys/p/12921641.html

猜你喜欢

转载自blog.csdn.net/qq_30549099/article/details/107743074