描述:客户端提供页码和每页大小,服务器根据总数计算出偏移量进行查询。
SQL:SELECT ... FROM table ORDER BY created_time DESC LIMIT {pageSize} OFFSET {(page-1)*pageSize};
API:GET /items?page=2&page_size=20
响应:通常包含 items(数据列表), total(总记录数), total_pages(总页数)。
直观易用:概念简单,符合用户对“书页”的认知。用户可以轻松跳转到任意页面(如第5页)。
状态无关:查询不依赖于特定记录,任何时间请求同一页码,返回的都是该“位置段”的数据。
易于实现统计:自然地提供总数、总页数等信息,便于前端显示“共100条,1/5页”。
数据一致性视图:在数据静态或变化缓慢的场景下,能提供一个稳定的数据视图。
性能问题(核心缺点):
OFFSET 在大数据量下效率低。数据库需要扫描并跳过 OFFSET 之前的所有行,即使你只取 LIMIT 条。OFFSET 10000 意味着数据库要先读取并丢弃一万条记录。
计算 COUNT(*) 在数据量大时也非常耗时。
数据重复/丢失问题(核心显示问题):
这是最致命的问题,尤其是在数据频繁增删的动态列表中(如社交动态、新闻推送)。
场景:用户正在看第一页(ID为100-81),此时在第一页之前插入了一条新记录(ID=101)。当用户翻到第二页时,数据库会用 OFFSET 20 去查询,此时原第一页的最后一条(ID=81)会再次出现在第二页的头部,导致数据重复。
反之,如果在翻页过程中第一页的数据被删除,那么第二页的数据会整体前移,导致用户丢失原本应该看到的一条记录(跳过了某条数据)。
这给用户的体验是列表“跳动”或不连贯。
描述:客户端提供上一页最后一条记录的某个唯一、有序的字段值(游标,通常是ID或时间戳),服务器查询“该游标之后”的N条记录。
SQL:SELECT ... FROM table WHERE id < {last_id} ORDER BY id DESC LIMIT {pageSize};
API:GET /items?cursor=123&page_size=20 (或 since_id=123, last_id=123)
响应:通常只包含 items(数据列表),以及用于下一页的游标(如最后一条的ID)。
高性能:查询利用了WHERE条件在索引上的定位,直接找到起始点开始读取,无需扫描和跳过大量行。性能与偏移量无关,仅和LIMIT大小有关。
数据一致性(解决核心显示问题):
非常适合动态数据流。因为查询基于某个时间点的数据快照(游标值),在连续的翻页过程中,即使有新增或删除,也不会影响当前“下一页”的查询结果,避免了重复和丢失。
提供了稳定的浏览体验,用户看到的内容是连贯的。
无法直接跳页:用户只能“上一页”、“下一页”地导航,无法直接跳转到任意中间页面(如第5页)。这对管理后台等需要跳转的场景不友好。
不提供总数:由于不进行COUNT操作,且数据动态变化,总页数或总条数通常无法提供或不准确。
游标字段要求高:
必须基于一个唯一且有序的字段(如自增ID、时间戳)。
如果排序涉及多个非唯一字段(如按score排序,score相同时按id排序),游标处理会变得复杂(需要组合条件,如 WHERE (score, id) < (?, ?))。
数据处理复杂度:需要客户端维护游标状态。API响应和参数设计需要仔细,以传递正确的“上一页”和“下一页”游标。
边界情况处理:如果游标指向的记录被删除,需要设计兜底逻辑(例如,如果根据ID找不到记录,则回退到时间范围查询)。
| 特性 | 页码/偏移分页 | 游标/键集分页 |
| 性能 | 大数据量下差(OFFSET慢) | 优秀,适合大数据集 |
| 跳页 | 支持 | 不支持,只能顺序翻页 |
| 数据总数 | 容易提供 | 难以提供,通常不提供 |
| 数据一致性 | 差,动态数据下会重复/丢失 | 优秀,适合动态数据流 |
| 实现复杂度 | 简单 | 较复杂 |
| 适用场景 | 后台管理系统、数据变化慢的列表、需要跳页和统计的场景 | 社交动态流、实时消息、新闻推送、任何无限滚动列表 |
你的数据表相对静态,更新不频繁。
用户需要跳转到特定页面(如后台数据查询)。
必须显示总条数和总页数。
数据量不大,性能不是首要考量。
你的数据是实时、动态的(如Twitter时间线、商品评论)。
你使用无限滚动作为前端交互。
数据量非常大,且对性能和浏览连贯性要求极高。
可以接受不显示总数和无法任意跳页。
在一些复杂场景下,可以结合使用。例如,管理后台默认用页码分页,但在“操作日志”这种可能大量插入数据的子模块中使用游标分页。或者,第一页用页码查询,后续翻页使用游标。
描述:不是独立的全新方式,而是在特定场景下对基础方案的组合与增强。
| 方案名称 | 核心优化思路 | 关键优点 | 适用场景 |
| 游标分页的深度优化 | 结合业务规则,用主键或时间范围替代OFFSET。 | 性能最佳,数据连贯,无需全局排序。 | 按时间线、ID顺序浏览的列表(如信息流)。 |
| 业务降级方案 | 对用户需求进行妥协或简化,如限制查询深度、提供替代路径。 | 实现简单,能规避最难处理的“深分页”性能悬崖。 | C端产品,用户容忍度较高的场景。 |
| 分布式环境优化 | 在分库分表或数据仓库中,采用流式归并和查询缓存。 | 缓解分布式全局排序的“数据爆炸”问题,降低重复计算。 | 分库分表的大数据量后台,固定报表导出 |
游标分页的深度优化 这是最高效的方案,尤其适合连续浏览的场景。关键在于将WHERE条件与排序字段对齐并建立索引。例如,按时间倒序查新闻:
sql
-- 首页SELECT * FROM news WHERE publish_time < NOW() ORDER BY publish_time DESC LIMIT 20;-- 下一页:用上一页最后一条的时间作为游标SELECT * FROM news WHERE publish_time < '上一页最后时间' ORDER BY publish_time DESC LIMIT 20;
注意:它牺牲了随机跳页的能力。如果需要跳页,可结合“业务降级方案”,改用其他查询路径(如搜索)。
业务降级方案 当技术优化遇到瓶颈时,从产品逻辑上着手往往更有效。
限制最大页码:例如,搜索和电商平台通常只允许查看前100页,引导用户使用更精确的筛选条件。
提供替代路径:在列表旁提供“按时间筛选”、“按标签筛选”或搜索框,让用户直接定位目标区域,避免连续翻页。
分布式环境优化 在分库分表或数据仓库(如AnalyticDB)中,深分页性能问题会被放大,因为数据需要在多个节点排序合并。
流式归并:像Apache ShardingSphere这样的中间件,采用流式处理从各分片按顺序获取数据再归并,避免将所有数据加载到内存。
查询缓存:阿里云AnalyticDB的Paging Cache功能是一个典型方案。首次查询时将排序结果存入缓存,后续相同模式的分页直接读取缓存,这对固定报表的多次分页导出场景性能提升巨大。
你可以根据数据量、场景和技术栈来决策:
| 数据规模与场景 | 推荐方案 | 原因与说明 |
| 中小数据量,需随机跳页 | 偏移分页 + 良好索引 | 简单直观。确保ORDER BY字段有索引,可配合数据库查询缓存。 |
| 大数据量,连续浏览 | 游标分页 | 性能和体验最佳。多数现代App信息流的首选。 |
| 分库分表,深分页查询 | 游标分页 + 中间件优化 | 优先用游标分页。若必须用偏移分页,需依赖ShardingSphere等中间件的流式归并优化。 |
| 固定报表导出 | Paging Cache类缓存方案 | 首次查询缓存全量排序结果,后续分页极快。 |
| C端产品,体验优先 | 游标分页 + 业务降级 | 用游标保证浏览流畅,同时提供筛选、搜索等路径替代深度跳页。 |
简单来说,对于追求极致性能的动态列表,深度优化的游标分页是首选。对于分库分表等复杂场景,可以借助中间件的流式处理能力。而对于后台批量操作,可以关注数据库厂商提供的 Paging Cache 等专有优化。