SQL深度分页问题案例实战

文章目录

概述

对比

特性 传统分页 游标分页
定义 使用 OFFSET 和 LIMIT 参数,通过跳过前面的记录来获取指定页的数据。 使用一个游标(cursor)来标记当前位置,基于游标位置获取后续数据。
特点 1. 需要知道总记录数(COUNT查询) 2. 使用页码(page)和每页数量(size) 3. 可以跳转到任意页面 1. 不需要总记录数 2. 使用游标(cursor)和每页数量(size) 3. 只能顺序翻页,不能跳转
优点 1. 可以跳转页面:用户可以直接跳转到第N页 2. 显示总数:可以显示总记录数和总页数 3. 实现简单:逻辑直观,易于理解 4. 兼容性好:所有数据库都支持 OFFSETLIMIT 1. 性能优秀: + 不需要 COUNT(*) 查询 + 查询速度稳定,不受数据量影响 + 使用索引高效定位 2. 数据一致性: + 基于游标位置查询,不受数据变化影响 + 不会出现重复或遗漏数据 3. 资源消耗低: + 不需要统计总数 + 查询效率高
缺点 1. 性能问题 : + COUNT(*) 查询在大数据量下很慢 + OFFSET 越大,查询越慢(需要跳过更多记录) 2. 数据一致性问题 : + 在翻页过程中,如果有数据新增或删除,可能导致: - 重复数据(同一数据出现在两页) - 遗漏数据(某些数据永远不会被看到) 1. 不能跳转页面:只能顺序翻页,不能直接跳转到第N页 2. 显示总数:无法显示总记录数和总页数 3. 实现复杂:需要处理游标编码/解码 4. 游标管理:需要确保游标的唯一性和稳定性
应用场景 1. 需要显示总数和总页数 + 商品列表需要显示"共1000件商品" + 订单列表需要显示"共50页" 2. 需要跳转页面 + 用户可以输入页码跳转 + 需要显示页码导航(1, 2, 3...) 3. 数据量不大 + 数据量在10万以内 + 查询频率不高 4. 管理后台 + 管理员需要查看总数 + 需要跳转到指定页面 1. 大数据量场景 + 数据量超过10万条 + 需要高性能查询 2. 移动端列表 + 无限滚动加载 + 不需要显示总数 3. 实时性要求高 + 数据频繁变化 + 需要保证数据一致性 4. C端应用 + 用户主要浏览最新数据 + 不需要跳转到历史页面 5. 时间线/动态流 + 微博、朋友圈等时间线 + 订单列表(按时间排序)

工作原理

sql 复制代码
-- 先查询总数
SELECT COUNT(*) FROM trade_order WHERE user_id = 'xxx';

-- 第一页(page=1, size=10)
SELECT * FROM trade_order 
WHERE user_id = 'xxx' 
ORDER BY create_time DESC 
LIMIT 10 OFFSET 0;

-- 第二页(page=2, size=10)
SELECT * FROM trade_order 
WHERE user_id = 'xxx' 
ORDER BY create_time DESC 
LIMIT 10 OFFSET 10;

执行流程:

  1. 执行 COUNT(*) 查询获取总记录数
  2. 根据页码计算 OFFSET = (page - 1) * size
  3. 执行主查询,跳过 OFFSET 条记录
  4. 返回当前页数据和总数
sql 复制代码
-- 第一页(无游标)
SELECT * FROM trade_order 
WHERE user_id = 'xxx' 
ORDER BY create_time DESC, id DESC 
LIMIT 11;  -- 查询11条,用于判断是否有更多数据

-- 第二页(使用游标)
SELECT * FROM trade_order 
WHERE user_id = 'xxx' 
AND (create_time < '2025-12-16 10:00:00' OR (create_time = '2025-12-16 10:00:00' AND id < 'xxx-uuid'))
ORDER BY create_time DESC, id DESC 
LIMIT 11;

执行流程:

  1. 如果有游标,解码游标获取 createTimeid
  2. 添加游标条件:create_time < cursor.createTime OR (create_time = cursor.createTime AND id < cursor.id)
  3. 查询 size + 1 条数据(多查1条用于判断是否有更多数据)
  4. 如果返回 size + 1 条,说明还有更多数据,返回前 size 条并生成下一个游标
  5. 如果返回 ≤ size 条,说明没有更多数据

性能对比

查询性能对比

说明:

  • 传统分页的 COUNT(*) 查询时间随数据量线性增长
  • 传统分页的 OFFSET 越大,查询越慢
  • 游标分页性能稳定,不受数据量和页码影响

数据库负载对比

操作 传统分页 游标分页
每次查询SQL数量 2条(COUNT + SELECT) 1条(SELECT)
COUNT查询 需要全表扫描或索引扫描 不需要
OFFSET操作 需要跳过N条记录 不需要
索引利用 部分利用 完全利用

代码示例

传统分页示例

请求
plain 复制代码
POST /api-portal/trade/order/page
{
  "page": 2,
  "size": 10,
  "status": 10,
  "createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"]
}
响应
plain 复制代码
{
  "code": 0,
  "data": {
    "list": [...],
    "total": 1000,
    "page": 2,
    "size": 10,
    "pages": 100
  }
}
SQL执行
plain 复制代码
-- 1. 查询总数
SELECT COUNT(*) FROM trade_order 
WHERE user_id = 'xxx' AND status = 10 
  AND create_time BETWEEN '2025-11-16' AND '2025-12-16';

-- 2. 查询数据
SELECT * FROM trade_order 
WHERE user_id = 'xxx' AND status = 10 
  AND create_time BETWEEN '2025-11-16' AND '2025-12-16'
ORDER BY create_time DESC 
LIMIT 10 OFFSET 10;

游标分页示例

首次请求(无游标)
plain 复制代码
POST /api-portal/trade/order/cursor-page
{
  "size": 10,
  "status": 10,
  "createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"]
}
响应
plain 复制代码
{
  "code": 0,
  "data": {
    "list": [...],
    "nextCursor": "MjAyNS0xMi0xNlQxMDowMDowMHw2ZTdhNTVlYi0zMzc0LTRjMDYtYmEzZi1mZGUwMmU5MGU5MWU=",
    "hasMore": true
  }
}
后续请求(使用游标)
plain 复制代码
POST /api-portal/trade/order/cursor-page
{
  "cursor": "MjAyNS0xMi0xNlQxMDowMDowMHw2ZTdhNTVlYi0zMzc0LTRjMDYtYmEzZi1mZGUwMmU5MGU5MWU=",
  "size": 10,
  "status": 10,
  "createTime": ["2025-11-16 00:00:00", "2025-12-16 00:00:00"]
}
SQL执行
plain 复制代码
-- 查询 size + 1 条数据
SELECT * FROM trade_order 
WHERE user_id = 'xxx' 
  AND status = 10
  AND create_time BETWEEN '2025-11-16' AND '2025-12-16'
  AND (create_time < '2025-12-16 10:00:00' 
       OR (create_time = '2025-12-16 10:00:00' AND id < 'xxx-uuid'))
ORDER BY create_time DESC, id DESC 
LIMIT 11;

⚠️ 需要注意的问题:

  1. 游标设计
  • 游标必须唯一且稳定(使用 createTime + id 组合)
  • 游标字段必须有索引
  • 使用 Base64 编码保护游标
  1. 排序字段
  • 必须使用唯一字段作为排序依据(如 id
  • 避免使用可能重复的字段(如 createTime 单独排序)
  1. 游标失效
  • 如果数据被删除,游标可能失效
  • 需要处理游标解析失败的情况
  1. 关键字查询
  • JOIN 查询时需要注意性能
  • 使用 DISTINCT 去重

游标分页最佳实践

推荐做法:

  1. 游标格式
plain 复制代码
// 使用 createTime|id 格式,Base64编码
cursor = Base64.encode("2025-12-16T10:00:00|uuid-string")
  1. 排序规则
plain 复制代码
ORDER BY create_time DESC, id DESC
-- 确保排序的唯一性和稳定性
  1. 游标条件
plain 复制代码
WHERE (create_time < cursor.createTime 
       OR (create_time = cursor.createTime AND id < cursor.id))
  1. 判断是否有更多数据
plain 复制代码
// 查询 size + 1 条
List<Order> orders = query(size + 1);
boolean hasMore = orders.size() > size;
if (hasMore) {
    orders = orders.subList(0, size);
    nextCursor = createCursor(orders.get(size - 1));
}

总结

选择建议

场景 推荐方案 原因
移动端列表(无限滚动) 游标分页 性能好,数据一致
管理后台(需要总数) 传统分页 需要显示总数和跳转
大数据量(>10万) 游标分页 性能优势明显
小数据量(<10万) 传统分页 实现简单
实时数据流 游标分页 数据一致性好
需要跳转页面 传统分页 游标分页不支持
相关推荐
小张快跑。2 小时前
【Java企业级开发】(十一)企业级Web应用程序Servlet框架的使用(上)
java·前端·servlet
星星不打輰2 小时前
SSM项目--SweetHouse 甜蜜蛋糕屋
java·spring·mybatis·ssm·springmvc
一位代码2 小时前
mysql | 常见日期函数使用及格式转换方法
数据库·mysql
Knight_AL2 小时前
Java 线程池预热(Warm-up)实战:开启与不开启到底差多少?
java·开发语言
爬山算法2 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
SelectDB2 小时前
Apache Doris 4.0.2 版本正式发布
数据库·人工智能
C++业余爱好者2 小时前
公司局域网访问外网的原理
java
杰克尼2 小时前
mysql_day01
数据库·mysql
@淡 定2 小时前
异常处理最佳实践
java