全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-11 8 分钟 ✍️ juanwangdev

Java大数据量分页查询

大数据量分页查询是性能优化的重点,不同数据库和场景需要不同策略。

分页SQL实现

MySQL分页

SQL
-- 基础分页(LIMIT offset, size)
SELECT * FROM users ORDER BY id LIMIT 0, 10;     -- 第1页
SELECT * FROM users ORDER BY id LIMIT 10, 10;    -- 第2页
SELECT * FROM users ORDER BY id LIMIT 100, 10;   -- 第11页

-- 简化写法(LIMIT size OFFSET offset)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 100;

-- 计算公式
offset = (page - 1) * pageSize

Oracle分页

SQL
-- Oracle 12c+
SELECT * FROM users ORDER BY id OFFSET 100 ROWS FETCH NEXT 10 ROWS ONLY;

-- 传统方式(ROWNUM)
SELECT * FROM (
    SELECT a.*, ROWNUM rn FROM (
        SELECT * FROM users ORDER BY id
    ) a WHERE ROWNUM <= 110
) WHERE rn > 100;

PostgreSQL分页

SQL
-- 与MySQL类似
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 100;

SQL Server分页

SQL
-- SQL Server 2012+
SELECT * FROM users ORDER BY id OFFSET 100 ROWS FETCH NEXT 10 ROWS ONLY;

-- 传统方式
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER(ORDER BY id) as rn FROM users
) t WHERE rn BETWEEN 101 AND 110;

分页性能问题

LIMIT深分页问题

SQL
-- 第1页:很快
SELECT * FROM users ORDER BY id LIMIT 0, 10;

-- 第100000页:很慢!
SELECT * FROM users ORDER BY id LIMIT 1000000, 10;

-- 原因:
-- MySQL先扫描1000010条记录
-- 然后丢弃前1000000条
-- 只返回最后10条
-- 扫描量与offset成正比

性能对比

offset扫描记录数执行时间
0101ms
1000010010100ms
1000001000101s
1000000100001010s

分页优化方案

方案1:子查询优化

SQL
-- 原SQL(慢)
SELECT * FROM users ORDER BY id LIMIT 1000000, 10;

-- 优化:子查询先获取ID
SELECT * FROM users WHERE id >= (
    SELECT id FROM users ORDER BY id LIMIT 1000000, 1
) ORDER BY id LIMIT 10;

-- 原理:
-- 子查询只扫描ID列(覆盖索引)
-- 主查询用ID定位,直接扫描10条

方案2:JOIN优化

SQL
-- JOIN方式
SELECT u.* FROM users u
INNER JOIN (
    SELECT id FROM users ORDER BY id LIMIT 1000000, 10
) t ON u.id = t.id;

-- 原理:同上,子查询只扫描索引

方案3:游标分页(上次位置)

SQL
-- 传统分页
SELECT * FROM users ORDER BY id LIMIT 1000000, 10;

-- 游标分页(记录上次最大ID)
SELECT * FROM users WHERE id > 1000000 ORDER BY id LIMIT 10;

-- 优势:
-- 直接用索引定位
-- 不需要扫描offset条记录
-- 翻页性能稳定

-- 缺点:
-- 只能下一页,不能跳页
-- 需要记录上次位置

方案4:索引覆盖

SQL
-- 无索引:全表扫描
SELECT * FROM users ORDER BY create_time LIMIT 100000, 10;

-- 有索引:索引扫描
CREATE INDEX idx_create_time ON users(create_time);
SELECT * FROM users ORDER BY create_time LIMIT 100000, 10;

-- 覆盖索引:更优
CREATE INDEX idx_cover ON users(create_time, id, name);
SELECT id, name FROM users ORDER BY create_time LIMIT 100000, 10;
-- 不需要回表查询

Java分页实现

基础分页

Java
public PageResult<User> findPage(int page, int pageSize) {
    int offset = (page - 1) * pageSize;

    String sql = "SELECT * FROM users ORDER BY id LIMIT ?, ?";
    List<User> users = jdbcTemplate.query(sql,
        new BeanPropertyRowMapper<>(User.class), offset, pageSize);

    String countSql = "SELECT COUNT(*) FROM users";
    int total = jdbcTemplate.queryForObject(countSql, Integer.class);

    return new PageResult<>(users, total, page, pageSize);
}

子查询优化分页

Java
public PageResult<User> findPageOptimized(int page, int pageSize) {
    int offset = (page - 1) * pageSize;

    // 子查询先获取ID
    String sql = "SELECT * FROM users WHERE id >= (" +
                 "SELECT id FROM users ORDER BY id LIMIT ?, 1" +
                 ") ORDER BY id LIMIT ?";

    List<User> users = jdbcTemplate.query(sql,
        new BeanPropertyRowMapper<>(User.class), offset, pageSize);

    String countSql = "SELECT COUNT(*) FROM users";
    int total = jdbcTemplate.queryForObject(countSql, Integer.class);

    return new PageResult<>(users, total, page, pageSize);
}

游标分页

Java
public List<User> findNextPage(Long lastId, int pageSize) {
    String sql = "SELECT * FROM users WHERE id > ? ORDER BY id LIMIT ?";
    return jdbcTemplate.query(sql,
        new BeanPropertyRowMapper<>(User.class), lastId, pageSize);
}

// 使用
List<User> page1 = findNextPage(0L, 10);      // 首页
Long lastId = page1.get(page1.size() - 1).getId();
List<User> page2 = findNextPage(lastId, 10);  // 下一页

MyBatis分页

XML
<!-- 基础分页 -->
<select id="findPage" resultType="User">
    SELECT * FROM users ORDER BY id LIMIT #{offset}, #{pageSize}
</select>

<!-- 子查询优化 -->
<select id="findPageOptimized" resultType="User">
    SELECT * FROM users WHERE id >= (
        SELECT id FROM users ORDER BY id LIMIT #{offset}, 1
    ) ORDER BY id LIMIT #{pageSize}
</select>

<!-- 游标分页 -->
<select id="findNextPage" resultType="User">
    SELECT * FROM users WHERE id > #{lastId} ORDER BY id LIMIT #{pageSize}
</select>

PageHelper插件

Java
// 使用PageHelper(MyBatis插件)
PageHelper.startPage(page, pageSize);
List<User> users = userMapper.selectAll();

PageInfo<User> pageInfo = new PageInfo<>(users);
// pageInfo.getList()    数据列表
// pageInfo.getTotal()   总记录数
// pageInfo.getPages()   总页数

分页策略选择

场景推荐方案说明
小数据量(<10000)LIMIT offset简单够用
中数据量(10000-100000)子查询优化性能提升明显
大数据量(>100000)游标分页性能最优
移动端无限滚动游标分页不需要跳页
传统分页跳页子查询优化支持跳页

总数查询优化

避免每次COUNT

Java
// 不推荐:每次查询COUNT
public PageResult<User> findPage(int page) {
    List<User> users = ...;
    int total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
    return new PageResult<>(users, total);
}

// 推荐:总数缓存或估算
// 1. 缓存总数(数据变化时更新)
// 2. 估算总数(适用于大表)
// 3. 不显示总数(只显示上一页/下一页)

总数估算

SQL
-- MySQL估算
SHOW TABLE STATUS LIKE 'users';
-- Rows列是估算值

-- 或使用EXPLAIN
EXPLAIN SELECT * FROM users;
-- rows列是估算扫描行数

注意事项

深分页(offset大)性能差,需优化

游标分页不支持跳页,适合无限滚动

总数COUNT在大表上很慢,可缓存或估算

分页必须有ORDER BY,否则结果不稳定

ORDER BY字段应有索引,否则全表扫描

覆盖索引避免回表,性能更优

要点总结

  1. MySQL LIMIT offset在大offset时性能差
  2. 子查询优化先获取ID,减少扫描量
  3. 游标分页记录上次位置,性能最优
  4. 索引覆盖避免回表,提升性能
  5. 大表总数COUNT慢,可缓存或估算

📝 发现内容有误?点击此处直接编辑

← 上一篇 Java事务管理与隔离级别
下一篇 → Java数据库索引优化
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库