MySQL的几种分页方式,你知道几种方式
作者:码农桃子
分页方式
MySQL目前常用的分页方式有两种:
- 利用limit实现分页,语法为“SELECT*FROM 表名 limit 开始记录数,每页条数”;
- 利用主键索引实现分页,语法为“SELECT*FROM 表名 WHERE 字段名 > (页数*10) LIMIT 条数”
一般使用第一种方式居多,适用于数据量不大的场景:
-- 0是开始的记录数,10是条数SELECT * FROM user LIMIT 0,10;
如果换成第二种写法:
SELECT * FROM user WHERE id > (0*10) LIMIT 10
id是主键。如果是第X页共Y条:(X从0开始计算)
SELECT * FROM user WHERE id > (X*10) LIMIT Y
当然,这种写法存在一定问题,如果第0页的id=5的数据被删除了,就会导致查询第0页的数据和第1页的数据有重合,第0页是1-4,6-11(默认一页10条数据,因为limit 10,所以会查询到id=11),第二页就是11-20,可见id=11重合了。
当然关于id不连续的问题,你可以“逻辑删除”,增加一个 isdel 的属性,当isdel=1时表明该数据“已经删除”。
但是对于大量数据来说,这种问题是可以忽视的。
主要的是,这种大量数据的表,是机会不会去对其进行删除甚至修改操作的。
那么为什么大量数据使用第二种更合理呢?我们使用MySQL的关键字explain来看一下大概。
EXPLAIN SELECT * FROM test LIMIT 0,10
其他参数不做说明,就看type:扫描类型
效率从最好到最坏依次是:system > const > eqref > ref > fulltext > refornull > indexmerge > uniquesubquery > indexsubquery > range > index > ALL
可见limit的分页效果最差。
当然,实际当中你可以增加一些查询参数,让查询不再走全表扫描。
做对比的话就不增加查询条件了。
EXPLAIN SELECT * FROM test WHERE id > (0*10) LIMIT 10
这个走的是range 范围查询 明显好多了。
但这实际上还是通过主键的查询,取巧了而已。并且主键有主键索引,查询更快。
一般情况下,得保证查询至少达到range级别,最好能达到ref。
第一种方式和第二种方式当数据量不多的时候,是第一种方式占优势,毕竟没有数据重合的问题,并且查询的速度也没有明显差别。但是当数据量上去了,差别就很明显了,
SELECT * FROM test LIMIT 0,10
和
SELECT * FROM test LIMIT 100,10
查询速度是不一样的 ,查询了第100页时,实际上前99页都已经被扫描过。
所以第二种的方式更适用于大量数据的场景。
那么关于第一种,再多说一些。你可以通过增加一些查询参数去限制type,但是参数加的不好甚至还不如不加!牵涉到回表问题。
关于回表:
先了解一些B+树:
在这个树状结构里,我们需要关注的是,最下面一层节点,也就是 叶子结点 。而这个叶子结点里放的信息会根据当前的索引是 主键还是非主键 有所不同。
- 如果是 主键索引 ,它的叶子节点会存放完整的行数据信息。
- 如果是 非主键索引 ,那它的叶子节点则会存放主键,如果想获得行数据信息,则需要再跑到主键索引去拿一次数据,这叫 回表 。(实际上查询了两次表)
比如执行:
select * from page where user_name = "小白10";
会通过非主键索引去查询 user_name 为" 小白10 "的数据,然后在叶子结点里找到" 小白10 "的数据对应的 主键为10 。
此时回表到 主键索引 中做查询,最后定位到 主键为10的行数据 。
但不管是主键还是非主键索引,他们的叶子结点数据都是 有序的 。比如在主键索引中,这些数据是根据主键id的大小,从小到大,进行排序的。
所以说,如果你加的查询参数是无索引,是无序的,那么就是“不如不加”。
那么第一种如何的去优化呢?
上面select后面带的是 星号 *,也就是要求获得行
select * from page where id >=(6000000) order by id limit 10;
数据的 所有字段信息。
我们结合第二种可以得到一种优化,比如执行的是:
select * from page order by id limit 6000000, 10;
由于这次的offset=6000000,会在innodb里的主键索引中获取到第0到(6000000 + 10)条 完整行数据, 从引擎层获取到 很多无用的数据 ,而获取的这些无用数据都是要耗时的。
当select后面是*号时,就需要拷贝完整的行信息, 拷贝完整数据 跟 只拷贝行数据里的其中一两个列字段 耗时是不同的,这就让原本就耗时的操作变得更加离谱。
因为前面的offset条数据最后都是不要的,就算将完整字段都拷贝来了又有什么用呢,所以我们可以将sql语句修改成下面这样:
select * from page where id >=(select id from page order by id limit 6000000, 1) order by id limit 10;
上面这条sql语句,里面先执行子查询 select id from page order by id limit 6000000, 1 , 这个操作,其实也是将在innodb中的主键索引中获取到 6000000+1 条数据,然后server层会抛弃前6000000条,只保留最后一条数据的id。
但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。
在拿到了上面的id之后,假设这个id正好等于6000000,那sql就变成了
select * from page where id >=(6000000) order by id limit 10;
这样innodb再走一次 主键索引 ,通过B+树快速定位到id=6000000的行数据,时间复杂度是lg(n),然后向后取10条数据。
这样性能确实是提升了,亲测能快一倍左右,属于那种耗时从3s变成1.5s的操作。
这······
属实有些杯水车薪,有点搓,属于没办法中的办法。
基于非主键索引的limit执行过程
上面提到的是主键索引的执行过程,我们再来看下基于 非主键索引 的limit执行过程。
比如下面的sql语句
select * from page order by user_name limit 0, 10;
server层会调用innodb的接口,在innodb里的非主键索引中获取到第0条数据对应的主键id后, 回表 到主键索引中找到对应的完整行数据,然后返回给server层,server层将其放到结果集中,返回给客户端。
而当offset>0时,且offset的值较小时,逻辑也类似,区别在于,offset>0时会丢弃前面的offset条数据。
也就是说非主键索引的limit过程,比主键索引的limit过程,多了个回表的消耗。
但当offset变得非常大时,比如600万,此时执行explain。
非主键索引offset值超大时走全表扫描
可以看到type那一栏显示的是ALL,也就是 全表扫描 。
这是因为server层的 优化器 ,会在执行器执行sql语句前,判断下哪种执行计划的代价更小。
很明显,优化器在看到非主键索引的600w次回表之后,摇了摇头,还不如全表一条条记录去判断算了,于是选择了全表扫描。
因此,当limit offset过大时,非主键索引查询非常容易变成全表扫描。是真·性能杀手。
这种情况也能通过一些方式去优化。比如
select * from page t1, (select id from page order by user_name limit 6000000, 100) t2 WHERE t1.id = t2.id;select id from page order by user_name limit 6000000, 100
先走innodb层的user_name非主键索引取出id,因为只拿主键id, 不需要回表 ,所以这块性能会稍微快点,在返回server层之后,同样抛弃前600w条数据,保留最后的100个id。然后再用这100个id去跟t1表做id匹配,此时走的是主键索引,将匹配到的100条行数据返回。这样就绕开了之前的600w条数据的回表。
当然,跟上面的case一样,还是没有解决要白拿600w条数据然后抛弃的问题,这也是非常挫的优化。
像这种,当offset变得超大时,比如到了百万千万的量级,问题就突然变得严肃了。
这里就产生了个专门的术语,叫 深度分页 。
总结
limit offset, size 比 limit size 要慢,且offset的值越大,sql的执行速度越慢。
当offset过大,会引发 深度分页 问题,目前不管是mysql还是es都没有很好的方法去解决这个问题。只能通过限制查询数量或分批获取的方式进行规避。
遇到深度分页的问题,多思考其原始需求,大部分时候是不应该出现深度分页的场景的,必要时多去影响产品经理。
如果数据量很少,比如1k的量级,且长期不太可能有巨大的增长,还是用 limit offset, size 的方案吧,整挺好,能用就行。
您可能感兴趣的文章:
- MySql分页时使用limit+order by会出现数据重复问题解决
- 为什么MySQL分页用limit会越来越慢
- mysql分页的limit参数简单示例
- 浅谈MySQL分页Limit的性能问题
- MySQL分页Limit的优化过程实战
- mysql分页性能探索
- 浅析Oracle和Mysql分页的区别
- SpringMVC+Mybatis实现的Mysql分页数据查询的示例
- 利用Spring MVC+Mybatis实现Mysql分页数据查询的过程详解
- mysql分页时offset过大的Sql优化经验分享
- MySQL分页分析原理及提高效率
- MySQL优化案例系列-mysql分页优化
- 你应该知道的PHP+MySQL分页那点事
- MYSQL分页limit速度太慢的优化方法
- MySQL分页优化
- MySQL分页技术、6种分页方法总结
- 8种MySQL分页方法总结
- mysql分页原理和高效率的mysql分页查询语句