分库分表下如何实现分页查询功能
分库分表(Sharding)环境下的分页查询功能,是工程实践中的一个难点和核心问题。由于数据被分散到不同的库和表中,简单的 LIMIT offset, limit
语句会带来巨大的性能和资源消耗。
以下是分库分表下实现分页查询的几种方案,以及对深度分页的优化:
一、 基本分页实现(通用方案)
对于一般的中间件(如 ShardingSphere、MyCAT 等),它们在处理分页查询时,通常采用“先查后合”的策略:
1. 跨库查询与结果归并
假设要查询第 NNN 页,每页 PPP 条记录(即 LIMIT (N-1)*P, P
):
- 子查询改写: 中间件会将一个查询请求(如
SELECT * FROM table ORDER BY col LIMIT offset, limit
)拆分成对所有分表的子查询。 - 子查询执行: 对每个分库分表执行一个扩大了查询范围的子查询:
SELECT * FROM table_i ORDER BY col LIMIT 0, offset + limit
。- 关键点: 必须从第 0 条开始查,并取到最终需要的末尾位置,以保证结果集的完整性。
- 结果归并: 中间件收集所有分表返回的结果集。
- 内存排序与截断: 在内存中对所有结果进行全局排序(
ORDER BY col
),然后根据原始查询的offset
和limit
进行截断,返回最终所需的数据。
存在的问题(深度分页问题):
当 offset
值很大时(即查询很靠后的页),子查询需要查询并返回大量数据。中间件在内存中归并和排序的数据量也会非常庞大,极大地消耗网络带宽、CPU 和内存资源,导致系统性能急剧下降。
二、 深度分页的优化方案(推荐)
为了避免在深度分页时拉取和排序海量数据,通常需要调整前端的交互方式或利用索引特性。
1. 利用 ID 排序的优化(推荐使用)
如果你的查询是基于主键(通常是自增 ID)进行排序,可以采用基于游标(Cursor)或上一页最后一条记录 ID 的分页方式,完全避开 offset
:
查询方式:
SELECT * FROM table
WHERE id > [上页最后一条记录的ID] -- 游标
ORDER BY id
LIMIT [每页记录数]
实现步骤:
- 前端交互改变: 不再提供“跳转到第 100 页”的功能,只提供“下一页”。
- 记录游标: 客户端(或服务端)需要记录当前页最后一条记录的 ID(作为下一页查询的起点)。
- 子查询优化: 由于
WHERE id > X
是基于索引的查询,数据库可以直接定位到起始位置,大大减少扫描的数据量。
优点: 性能高,避免了跨库归并大量数据的问题。
缺点: 只能应用于按 ID 排序的场景,并且无法实现“跳转页”和“上一页”(如果需要上一页,需要客户端记住前一页的游标)。
2. 利用 Sort Key 优化的方案
如果排序字段(Sort Key)不是 ID,但存在高效的二级索引,也可以借鉴游标法:
查询方式:
SELECT * FROM table
WHERE (sort_key, id) > ('[上页最后一条sort_key]', [上页最后一条ID])
ORDER BY sort_key, id
LIMIT [每页记录数]
这种方法是复合游标法,通过组合 排序键 + 主键 来确保查询的唯一性和连续性,同样能高效利用索引,避免深度的 offset
问题。
3. 两次查询法(适用于非 ID 排序)
当必须支持“跳转页”且排序字段不是 ID 时,可以采用两次查询来优化:
- 第一次查询(只查 ID):
SELECT id FROM table ORDER BY col LIMIT (N-1)*P, P
- 这一步仍然需要跨库归并,但由于只查询了主键 ID,数据量(带宽和内存消耗)大大减少。
- 第二次查询(查详情):
SELECT * FROM table WHERE id IN (第一次查询返回的ID列表)
- 根据 ID 列表进行查询,中间件可以根据 ID 路由到对应的分表,直接精确查询并返回数据。
优点: 减少了深分页时传输和归并的数据量,将大范围的全字段查询变为小范围的 ID 查询。
缺点: 引入了两次数据库查询,增加了整体耗时,并且第一次查询的归并排序仍然会消耗一定的 CPU 资源。
总结与建议
场景 | 推荐方案 | 核心优势 | 限制/注意事项 |
---|---|---|---|
按 ID 排序,只需“下一页” | 基于 ID 游标(Cursor) | 性能最高,完全避免深分页问题。 | 无法支持“跳转到任意页”。 |
按非 ID 字段排序,只需“下一页” | 基于 Sort Key + ID 游标 | 性能接近 ID 游标,仍可利用索引。 | 复杂排序(多列)场景不适用。 |
必须支持“跳转到任意页” | 两次查询法(ID Only Query) | 在支持跳转的前提下,最大限度节省传输和归并资源。 | 需要两次 DB 往返,且第一次查询仍需跨库归并。 |
查询页码较浅 (N<10) | 通用方案(先查后合) | 实现最简单,代码侵入性最小。 | 不适用于深度分页。 |