深度分页问题分析和优化

目录

  • 数据集
  • 深度分页问题
  • 深度分页优化
    • 1 通过子查询优化
    • 2 使用inner join优化
    • 3 使用标签记录优化
  • 小结
  • reference

数据集

test_db下载 :https://gitee.com/lzjcnb/test_db

数据集安装:mysql -uroot -p -t < employees.sql

深度分页问题

后端开发为了防止一次加载太多数据到内存,对内存占用和IO读取开销太大,一般使用limit 关键字进行分页加载数据(懒加载,再需要查看时再加载数据),查询employess表中第[N,N+m]条记录,先看下测试数据集大小:
深度分页问题分析和优化_第1张图片
分别查询[10000,10003]三条记录和[100000,100003]三条记录,并观察执行时间。

mysql> select * from employees limit 10000,3

深度分页问题分析和优化_第2张图片
发现在查询结果集数量一定的情况下,查询时间和查询深度成正比。

深度分页效率低原因: limit m,n查询过程是数据库的服务层的执行器调用存储引擎从磁盘中读取m+n条记录到服务层,然后服务层根据offset丢掉前m条,保留第【m,m+n】n条结果,并返回给客户端返回,从磁盘中读取m+n条数据是整个查询过程中耗时的操作,理论上分析limitm,n 方式查询m条数所用耗时和直接查询m+n数据时间应该接近,没有明显的时间差距。

深度分页优化

1 通过子查询优化

说明:在employees表中,emp_no是主键,也即聚集索引,在birth_date列创建一个普通非聚集索引。

mysql> create index birth_date_index on 
employees(birth_date);--在birth_date字段创建非聚集索引

查询语句1:

mysql> select * from employees 
where birth_date > '1955-01-01' 
order by emp_no 
limit 100000,3;

深度分页问题分析和优化_第3张图片
查询过程:
在这里插入图片描述
查询语句2:

mysql> select *  from employees
 where emp_no >=
 	 (select emp_no from employees
 	  where birth_date > '1955-01-01' 
 	  order by emp_no limit 100000,1) 
 	  limit 3;

深度分页问题分析和优化_第4张图片
查询过程:
深度分页问题分析和优化_第5张图片
小结:查询语句1和查询语句2实现同样的深度分页查询功能,观察查询效率,查询语句2是查询语句1的15倍。对比二者查询过程,查询语句2使用嵌套查询,在内层循环中只取聚簇索引列字段,确定深度分页的偏移量,此时不需要回表,在外层查询中使用聚簇索引筛偏移量之后的要查询结果集,查询语句2通过减少回表查询记录数,提高查询效率。
-------------2023.08.09更新-------------------
「查询语句2」的内层查询语句(select emp_no from employees where birth_date > '1995-01-01' order by emp_no limit 100000,1)存在内存排序问题,尤其当 子查询结果数量很大的时候,内存排序也很消耗资源,可能依然会引起慢查询,需要考虑进一步优化。
优化思路1:
既然子查询中使用到birth_date索引,如果按照birth_date进行排序,利用索引本身就是有序的特点就可以避免内存排序,对应子查询就变为sql select emp_no from employees where birth_date > '1955-01-01' order by birth_date limit 100000,3, 那这样子查询结果是有关emp_no的无序集合,外层循环可以对应修改成 select * from employees where emp_no in (sql select emp_no from employees where birth_date > '1955-01-01' order by birth_date limit 100000,3`),当单批查询数量较小,外层循环也会使用到聚簇索引,有的sql不支持子sql使用limit语句,可以将整sql拆分成两个语句,虽然可能由于多次查询数据库引起整体查询时间较长,但可以避免单条sql的慢查询,对于整体要求查询耗时不太高的任务依然是个不错的选择。
优化思路2:
利用同一个非聚簇索引叶子结点上聚簇索引值是有序的特点,每次查询时,我们保留最大的birth_date和最大的emp_no,sql语句如下
select * from employees where (birth_date > :birth_date and updatedTime < :end_time) or (birth_date = :birth_date and id > :id) order by birth_date, id asc limit 100 首先,sql是为了分批查询出生在一个时间段内的所有员工信息,sql走的是birth_date所在列索引,首先确定birth_date索引叶子结点的开始位置,如果碰到birth_date大于上批次最大的birth_date就满足条件,如果如果birth_date相等,并不会直接跳过这个节点,而是走or语句,判断这个叶子结点上是否存在id是否大于上一批次最大emp_no记录,存在的话,偏移量位置起始位置就是当前叶子节点,且从这个叶子结点emp_no大于上一批次最大emp_no位置开始取值,在由非聚簇索引最底层叶子结点组成的链表中,起始位置中间的元素就是我们满足我们查询条件的聚集索引id,然后根据birth_date和emp_no进行排序,巧合的是,此时的链表顺序就是我们要排列的顺序,避免了内存排序,直接从开始位置算,根据limit size大小沿着链表取一定数量的聚簇索引id,最后根据聚簇索引id,查询到这一批次完整记录信息。

2 使用inner join优化

查询语句:

mysql> select e1.* from employees e1 
inner join 
(select emp_no from employees
     where birth_date > '1955-01-01' 
     order by emp_no limit 100000,3) as e2  
 on e2.emp_no=e1.emp_no;

深度分页问题分析和优化_第6张图片

查询过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3N3skxXH-1647074518464)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4c9578e0-eff9-40ff-952e-918e92183dc3/Untitled.png)]
优化原理:与基于子查询的优化方式类似,也是通过减少回表的记录数提高查询效率。

3 使用标签记录优化

查询语句:

mysql> select * from employees 
where  birth_date >'1955-01-01' and emp_no > 229225
order by emp_no limit 3;

深度分页问题分析和优化_第7张图片

优化原理:在非第一次查询之后,查询时,要记录已经读取的记录数量,效率非常快,缺点是在查询过程中数据库数据变化可能导致查询的数据不准,并且要求字段自增,并且每次查询要知道上一次查询结果中的最大Id,所以不能跳页查看,只能前后翻页。

小结

综合三种深度分页优化方式,基于标记的深度分页方式优化效果最明显,sql也最简单,针对主键索引不是自增问题,可以先使用order by进行排序解决。

mysql深度分页查询语句模板:

select * from tab_name  
where 【条件1,条件2,条件3and id > #{lastId} 
order by id 
limit #{pageSize};

当数据量非常大的时候,where 查询速度也将变慢,基于ES的查询,可以提高查询效率,ES中同样存在深度分页的问题,在ES中使用了Scroll游标方式实现,具体实现原理todo…

reference

1 https://juejin.cn/post/7012016858379321358

你可能感兴趣的:(数据库,mysql,dba)