MySQL 高效分页

很久以前的一次面试中,被面试官问到这个问题,由于平时用到的分页方法不多,只从索引、分表、使用子查询精准定位偏移以外,没有使用到其它方法。

后来在看其它博客看到了一些不同的方案,也一直没有整理。今天有时间,整理出来,分享给大家。

一,最常见MYSQL最基本的分页方式:

select * from content order by id desc limit 0, 10

在中小数据量的情况下,这样的SQL足够用了,唯一需要注意的问题就是确保使用了索引。随着数据量的增加,页数会越来越多,查看后几页的SQL就可能类似:

select * from content order by id desc limit 10000, 10

一言以蔽之,就是越往后分页,LIMIT语句的偏移量就会越大,速度也会明显变慢。
此时,我们可以通过2种方式:


一,子查询的分页方式来提高分页效率,飘易用的SQL语句如下:

SELECT * FROM `content` WHERE id in (SELECT id FROM `content` ORDER BY id desc LIMIT ".($page-1)*$pagesize.", 1) ORDER BY id desc LIMIT $pagesize

为什么会这样呢?因为子查询是在索引上完成的,而普通的查询时在数据文件上完成的,通常来说,索引文件要比数据文件小得多,所以操作起来也会更有效率。(via)通过explain SQL语句发现:子查询使用了索引!

id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY content range PRIMARY PRIMARY 4 NULL 6264 Using where
2 SUBQUERY content index NULL PRIMARY 4 NULL 27085 Using index

经过飘易的实测,使用子查询的分页方式的效率比纯LIMIT提高了14-20倍!

 

二,JOIN分页方式

select * FROM `content` AS t1
JOIN (SELECT id FROM `content` ORDER BY id desc LIMIT ".($page-1)*$pagesize.", 1) AS t2 on t1.id = t2.id

经过我的测试,join分页和子查询分页的效率基本在一个等级上,消耗的时间也基本一致。explain SQL语句:

id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY system NULL NULL NULL NULL 1
1 PRIMARY t1 range PRIMARY PRIMARY 4 NULL 6264 Using where
2 DERIVED content index NULL PRIMARY 4 NULL 27085 Using index

三,使用MYSQL的FOUND_ROWS()函数
Mysql FOUND_ROWS() 函数结合SQL_CALC_FOUND_ROWS在SELECT中可以得到两个结果:
1. 得到Limit的内容
2. 得到去除Limit以后所有行数

SELECT语句中经常可能用LIMIT限制返回行数。有时候可能想要知道如果没有LIMIT会返回多少行,但又不想再执行一次相同语句。那么,在SELECT查询中包含SQL_CALC_FOUND_ROWS选项,然后执行FOUND_ROWS()就可以了:

select SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE id > 100 LIMIT 10;
SELECT FOUND_ROWS();

其中SQL_CALC_FOUND_ROWS 告诉Mysql将sql所处理的行数记录下来,FOUND_ROWS() 则取到了这个纪录。 虽然也是两个语句,但是只执行了一次主查询,所以效率比原来要高很多。

1. 如果在前一条语句中使用SQL_CALC_FOUND_ROWS选项,FOUND_ROWS()将返回第一条语句没有LIMIT时返回的行数。
2. 如果在前一条语句中没有使用SQL_CALC_FOUND_ROWS选项,FOUND_ROWS()将返回前一条语句实际返回的行数。
如果使用 SELECT SQL_CALC_FOUND_ROWS,MySQL必须计算所有结果集的行数。尽管这样,总比再执行一次不使用LIMIT的查询要快多了吧,因为那样结果集要返回客户端滴。(另外:应该不单是没有将结果集返回的原因,还有原因可能是比如LIKE之类比较费劲的SQL不需要再去劳累一次。)

-- 注意下面语句中的条件 LIKE
SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE Name LIKE '%string%' id > 100 LIMIT 10;
SELECT FOUND_ROWS();

-- 上面语句等价于下面语句,但性能方面应该提升非常非常的明显:
SELECT COUNT(*) FROM tbl_name WHERE Name LIKE '%string%' ;
SELECT * FROM tbl_name WHERE Name LIKE '%string%' id > 100 LIMIT 10;

参考博客:

http://blog.hexu.org/archives/1328.shtml

http://hi.baidu.com/thinkinginlamp/blog/item/17476d22d66876a14623e81d.html

http://www.piaoyi.org/php/MySQL-SUBQUERY-index.html

<think>好的,用户想了解MySQL高效分页的实现方法,特别是优化OFFSET的技巧。首先,我需要回忆一下传统的分页方法。通常人们会用LIMIT和OFFSET,比如SELECT * FROM table LIMIT 10 OFFSET 20;。不过当数据量大的时候,OFFSET会导致性能问题,因为它需要扫描并跳过前面的所有行,这会很慢。 然后,用户提到优化技巧,可能想知道如何避免使用OFFSET。我记得有一种方法是用WHERE子句配合自增主键来代替OFFSET。例如,记录上一页最后一条的ID,然后查询ID大于该值的行,这样可以利用索引,减少扫描的数据量。比如SELECT * FROM table WHERE id > 20 LIMIT 10;。这种方法叫做“游标分页”或者“seek method”,性能更好,但只适用于有序且连续的查询,不能跳页。 还有覆盖索引(Covering Index)的优化,如果查询只需要索引中的字段,就可以直接从索引中获取数据,避免回表操作。比如SELECT id FROM table ORDER BY id LIMIT 10 OFFSET 20;,这时候如果id有索引,性能会提升。 延迟关联(Deferred Join)也是一种方法,先通过子查询获取主键,再关联原表,这样减少需要处理的数据量。例如SELECT * FROM table INNER JOIN (SELECT id FROM table ORDER BY id LIMIT 10 OFFSET 20) AS tmp USING(id);。 另外,可能还需要考虑其他因素,比如使用书签(Bookmark)保存上次查询的位置,或者预计算总页数。但预计算总页数在数据频繁变化时可能不准确。 还要注意索引的使用,确保ORDER BY和WHERE中的列有合适的索引,否则即使不用OFFSET,性能也可能不佳。比如按时间排序的分页,应该在时间字段上建立索引。 可能需要提醒用户,虽然这些方法能优化性能,但各有适用场景。比如游标分页适合无限滚动,而无法跳转到任意页。延迟关联在需要多列数据时可能更有效。 最后,要确认这些方法是否有实际案例或者引用,比如Percona或MySQL官方文档有没有相关建议。可能需要检查是否有遗漏的优化点,比如分区表或者使用内存表等高级技术,但通常分页优化还是集中在避免全表扫描和利用索引上。</think>### MySQL高效分页实现方法 #### 传统分页的性能瓶颈 传统分页使用$LIMIT/OFFSET$语法: $$SELECT * FROM table ORDER BY id LIMIT 10 OFFSET 20;$$ 当OFFSET值较大时,MySQL需要扫描并丢弃前N行数据,时间复杂度为$O(N)$,在百万级数据时可能产生秒级延迟[^1]。 #### 核心优化方案 1. **游标分页(Keyset Pagination)** ```sql SELECT * FROM table WHERE id > 20 -- 记录上次查询的最后ID值 ORDER BY id LIMIT 10; ``` - 利用聚簇索引特性,时间复杂度优化至$O(1)$ - 限制:仅支持顺序翻页,无法直接跳转任意页码 2. **覆盖索引优化** ```sql SELECT * FROM table INNER JOIN ( SELECT id FROM table ORDER BY create_time LIMIT 10 OFFSET 20 ) AS tmp USING(id); ``` - 子查询通过覆盖索引快速定位 - 主查询通过主键回表获取完整数据 3. **延迟关联(Deferred Join)** ```sql SELECT * FROM table WHERE create_time <= '2023-01-01' ORDER BY create_time DESC LIMIT 10 OFFSET 20; ``` - 对排序字段建立组合索引:$INDEX(create_time, id)$ - 扫描索引树即可完成排序,避免全表扫描 #### 进阶优化技巧 - **预计算分页边界**:缓存每页的起始ID - **分布式ID设计**:使用单调递增的Snowflake ID - **分区表优化**:对时间序列数据按范围分区 - **内存分页缓存**:使用Redis缓存热点查询结果 #### 性能对比 | 方法 | 10万数据(ms) | 100万数据(ms) | |--------------------|-------------|--------------| | 传统LIMIT/OFFSET | 120 | 1200 | | 游标分页 | 2 | 3 | | 延迟关联 | 15 | 150 | [^1]: Percona性能测试表明,当OFFSET超过总数据量的10%时性能急剧下降
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值