前言
大家好,这里是程序员阿亮
大家肯定都用limit进行过分页,这个分页实际上在MySQL之中是一个很笨拙的过程!很容易造成深分页问题!
一、limit深分页问题
假设你有一条这样的 SQL 语句:
sql
SELECT * FROM orders ORDER BY create_time LIMIT 1000000, 10;
你主观上是不是认为MySQL 会直接跳到第 100 万行,然后拿出 10 条数据。但事实并非如此。
我们的MySQL会很笨拙地:
大量扫描: MySQL 必须从索引树上按顺序扫描获取 1,000,010(100万零10)条记录。
如果order by的字段是二级索引就会需要大量回表,如果没有,通过filesort也需要大量的回表查询,将100万数据放到sortbuffer里面或者临时文件进行排序在获取最后的10条!
查出这 100万零10 条完整的记录后,MySQL 会丢弃前 100 万条,只将最后的 10 条返回给你。
绝大部分的磁盘 I/O 和 CPU 算力都浪费在了那 100 万条最终要被丢弃的数据上,尤其是"回表"操作带来的大量随机磁盘读取,直接拖垮了性能。
二、优化思路
1.游标分页 / 标签分页(Seek Method)
适用场景: 移动端下拉刷新、瀑布流、或者不需要"跳页"(如直接跳转到第 100 页)的场景。
这是性能最好的分页方式。它的核心思想是:记住上一页最后一条数据的特征(通常是连续的自增 ID 或时间戳),作为下一页的查询条件。
比如说:
sql
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
优化后:
sql
SELECT * FROM orders WHERE id > 1000050 ORDER BY id LIMIT 10;
这样就可以避免我们先获取前100万条数据后丢弃,直接通过索引去获取数据了!
2.子查询、延迟查询优化
适用场景: 必须保留"页码跳转"功能(比如 PC 端的传统分页组件)。
核心思想是:利用"覆盖索引"(Covering Index)极大地减少回表的开销。 我们先通过索引只查出需要的 ID,然后再用这些 ID 去关联原表获取完整数据。
优化前:
sql
SELECT * FROM orders ORDER BY create_time LIMIT 1000000, 10;
优化后:
sql
SELECT t1.* FROM orders t1
INNER JOIN (
-- 这里的子查询只查主键 id,如果 create_time 有索引,就能完美命中覆盖索引,无需回表
SELECT id FROM orders ORDER BY create_time LIMIT 1000000, 10
) t2 ON t1.id = t2.id;
通过这种方式,我们可以先在where条件建立联合索引,通过这个联合索引获取合适的ids,然后再去主键索引获取需要的10条数据,相对来说数据的开销就要小很多,因为在没有子查询的时候,我们的SQL需要获取所有的100万条数据,取出完整的行数据,然后在取最后10条
而我们的子查询则可以先获取符合条件的ids,通过我们的二级索引
然后再通过二级索引去主键索引获取数据,避免了对100万条数据的整行数据扫描。
-
优点: 子查询中因为只查
id,避免了前 100 万条记录的回表操作,都在内存/索引中完成,性能提升非常明显。然后主查询只需要拿这 10 个 ID 回表查具体数据即可。 -
缺点: 随着
offset变得极其庞大(比如千万级别),子查询扫描索引的开销依然不小,只是缓解了痛点。
三、总结
实际上我们业务中使用子查询已经可以将数据在很大程度上进行性能优化了,通过减少数据量与IO来提高我们的性能,也可以通过瀑布分页来达到更高的性能需求,但是这种情况下也就不能满足跳页。
