10人参与 • 2026-09-17 • Mysql
面试官考点分析:
limit offset, size 工作机制及执行计划的理解。面试官问: 请谈谈你对mysql深度分页问题的理解,以及如何解决?
你可以这样回答:
mysql的深度分页是指在分页查询时,limit 语句的偏移量offset非常大的情况。例如 select * from t_order limit 1000000, 10。其核心问题在于性能低下,因为mysql会读取并丢弃前100万条记录,只为返回最后10条,这造成了大量的i/o和cpu浪费。
解决这个问题的核心思路是“避免扫描并丢弃大量无用数据”。 常用的方案有几种:
where id > last_id 来替代 limit offset,直接从目标位置开始扫描。要理解优化方案,必须先明白 limit 1000000, 10 为什么慢。
假设我们有一张用户订单表 t_order,在 create_time 上建立了索引,但查询语句是 select * from t_order order by create_time limit 1000000, 10。
执行流程是这样的:
create_time 索引,从第一行开始顺序扫描。select * 要求的完整数据,mysql每发现一条数据,就需要根据主键去回表查询整行记录。这意味着它要对前100万条数据全部执行回表操作,即使这些数据最终会被丢弃。痛点总结: 深度分页的真正代价不在于“有多少条数据被返回”,而在于“有多少条数据被丢弃,但在丢弃前又不得不进行昂贵的回表操作”。
优化原理——延迟关联(deferred join):
优化后的sql如下:
select * from t_order
inner join (
select id from t_order
order by create_time
limit 1000000, 10
) as tmp on t_order.id = tmp.id;
它的执行逻辑完全改变了:
select id from t_order order by create_time limit 1000000, 10 只查询了主键id。非常重要的细节是,create_time 索引和主键id可以构成覆盖索引,这意味着整个子查询只需扫描索引树,完全不需要回表!这一步用极快的速度找到了目标页的10个主键id。一张图看懂区别:
| 方案 | 扫描数据量 | 回表次数 | 性能 |
|---|---|---|---|
limit 1000000, 10 | 100万 + 10 行 | 100万 + 10 次 | 极低 |
| 子查询优化 | 100万 + 10 行 | 10次 | 极高 |
这是用空间换时间的经典案例,让数据库引擎只做它最擅长的事:在索引中快速定位,而非搬运大量无用的完整行数据。
后台管理系统:
limit offset, size查询会导致数据库cpu飙升,接口响应超时。c端用户app/网页的“无限下拉”/瀑布流:
offset的使用。数据报表导出:
where id > last_id limit 1000的方式,循环分批获取数据,边取边写入文件流,实现稳定、低内存占用的导出。假设我们有一个订单实体类 order 和对应的mapper。
// 实体类
@data
@tablename("t_order")
public class order {
private long id;
private string orderno;
private bigdecimal amount;
private localdatetime createtime;
// ... 其他字段
}
// mapper接口
@mapper
public interface ordermapper extends basemapper<order> {
// 方法1:原生深度分页(反面教材)
list<order> selectpagebyoffset(@param("offset") long offset, @param("size") integer size);
// 方法2:子查询优化
list<order> selectpagebysubquery(@param("offset") long offset, @param("size") integer size);
// 方法3:标签记录法(游标分页)
list<order> selectpagebycursor(@param("lastid") long lastid, @param("size") integer size);
}mapper xml 配置:
<!-- 方法2的实现:子查询优化 -->
<select id="selectpagebysubquery" resulttype="com.example.entity.order">
select *
from t_order
inner join (
select id
from t_order
order by create_time desc
limit #{offset}, #{size}
) as tmp on t_order.id = tmp.id
order by t_order.create_time desc;
</select>service 层调用与解释:
@service
public class orderservice {
@autowired
private ordermapper ordermapper;
public list<order> getordersbypage(int page, int size) {
// 计算偏移量
long offset = (long) (page - 1) * size;
// 调用子查询优化方法
return ordermapper.selectpagebysubquery(offset, size);
}
}执行流程与注意事项:
order by 的字段上有索引,并且子查询中只 select 主键,才能构成覆盖索引。mapper xml 配置:
<!-- 方法3的实现:标签记录法 -->
<select id="selectpagebycursor" resulttype="com.example.entity.order">
select *
from t_order
<where>
<if test="lastid != null">
and id < #{lastid} -- 使用大于号还是小于号取决于排序方向,这里假设按id降序
</if>
</where>
order by id desc
limit #{size};
</select>service 层调用与解释:
@service
public class orderservice {
@autowired
private ordermapper ordermapper;
/**
* 获取第一页数据
*/
public list<order> getfirstpage(int size) {
return ordermapper.selectpagebycursor(null, size);
}
/**
* 获取下一页数据
* @param lastid 上一页最后一条数据的id
* @param size 每页大小
*/
public list<order> getnextpage(long lastid, int size) {
return ordermapper.selectpagebycursor(lastid, size);
}
}执行流程与注意事项:
lastid 之后直接开始扫描,完全避免了 offset,性能极高且稳定,不受数据量增长影响。create_time)。| 特性 | 原生 limit | 子查询优化 | 标签记录法 | 游标分页 (cursor-based) |
|---|---|---|---|---|
| 实现复杂度 | 极低 | 中 | 低 | 低 |
| 性能(深度分页) | 极差 | 良好 | 极好 | 极好 |
| 支持跳页 | 是 | 是 | 否 | 否 |
| 数据一致性 | 要求高 | 要求高 | 易受新增/删除数据影响 | 要求高 |
| 适用场景 | 小数据量后台 | 中大型后台管理 | c端无限下拉、瀑布流 | 数据导出、api分页接口 |
limit并配合适当的索引,完全足够。过度优化会增加系统复杂度。以上就是在mysql中监控和优化慢sql的完整指南的详细内容,更多关于mysql监控和优化慢sql的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论