上次在MySQL8上导入一个1000万数据之后,今天就想验证看看mysql对于单表大数据的分页的表现情况,并探讨一下单表大数据分页的优化思路。
简单说明下测试环境。
一般开发人员对于web分页的解决方案是使用limit子句来完成,语法为:
limit n,m
对于小规模数据应用来讲,可能并不会有大问题,在一定程度上还是比较有效率的,但是如果在大数据应用来讲,它的效率就可能很低了,因为它是全表扫描,数据越往后,即ID越大,数据越慢,例如下面的例子
select * from t_user LIMIT 90000 ,100 ##耗时 1秒 select * from t_user LIMIT 900000 ,100 ##耗时 2秒 select * from t_user LIMIT 9000000 ,100 ##耗时 6秒
耗时依次增加。
因为我们字段比较少,所以可能执行起来没想象中的慢,我们可以用执行计划来验证一下
explain select * from t_user LIMIT 1000000 ,100
从结果看出执行全表扫描,所以如果只取前面几条,则还是比较容易,如果是取相对靠后的数据,则全表扫描对数据库压力时非常大,特别web应用并发查询,则很容易造成数据库响应慢。
很明显,既然做全表扫描引起效率低下,那么我们第一印象肯定想办法是不是不走全表扫描。可以这么做,先在索引ID上完成分页操作,然后再根据ID关联回去。查询代码更新如下:
1.索引ID完成分页
select id from t_user order by id limit 9000000,100
2.最后完成自关联,最终SQL如下:
select * from t_user a inner join (select id from t_user order by id limit 9000000,100) b on a.id = b.id
3.看看执行计划,可见已经没有再全表扫描,这种方式是减少扫描来提高分页效率。
最终执行时间为3秒,效率提高一倍。
使用子查询进行优化,原理是通过子句先定位查询起始ID值,然后再通过主查询语句的ID往后查询,此方法适用ID排序的情况。举例如下:
select * from t_user where id>=(select id from t_user limit 9000000,1) limit 100;
通过执行计划查看,它也属于非全表扫描,所以效率同第一种优化方案
把LIMIT 查询转换成某个位置的查询,此思路需要和应用开发进行结合,不是单纯的数据库优化,因为我们实现的是web的分页查询,所以是可以通过页面的现实记录数和当前页,计算出该页的ID范围,我们拼出以下SQL
select * from t_user where id between 9000000 and 9000100 limit 100;
耗时只需0.4豪秒,是非常快速的查询,也是我们在生产中最常见的一种实现方法。
优化方案2和优化3其实都是把LIMIT m,n转换成 limit n的查询,适合排序字段唯一的情况,当然大部分场景我们的排序字段都会选主键ID,这样操作的确能减轻分页的压力。理论上,几千万数据在mysql8上还是可以支撑的。
如果在数据量更大的情况下,这几种方案都无法满足了,一般我们得在数据库层和应用层之间设置缓存数据库,再执行分页查询。