分库分表后如何处理排序、分页和聚合查询?
简化版
分库分表后的排序、分页、聚合如果跨多个分片,需要各分片先局部查询,再在应用或中间件层合并。深分页和全局聚合代价很高,通常要改成游标分页、限制查询范围、预聚合、异构分析库或报表系统。
详细版
常见处理方式:
- 查询带分片键:只查单分片,分页排序最简单;
- 多分片 TopN:每个分片取前 N 条,合并排序后再取全局 N;
- 避免深分页:使用游标、上次最大/最小排序键继续翻页;
- 聚合预计算:统计计数、排行榜、报表提前写入汇总表;
- 限制查询范围:按时间、租户、状态缩小扫描分片;
- 分析型查询走 ES、ClickHouse、Hive 等异构系统。
面试时要指出:跨分片 limit offset,size 可能被放大,例如第 1000 页每片都要取很多数据再合并,不能照搬单库分页思路。
完整版教学
一、单库分页和跨分片分页不是一个问题
单库里执行:
select * from orders order by create_time desc limit 10000, 20;
虽然深分页也慢,但至少排序和截取都在一个数据库实例内部完成。分库分表后,数据散在多个分片上。全局第 10000 到 10020 条,并不等于每个分片的第 10000 到 10020 条。
中间件为了得到正确结果,可能要让每个分片取出前 10020 条,再把所有结果合并排序,最后截取全局的 20 条。如果有 64 个分片,就可能拉回 64 × 10020 条候选数据,成本非常夸张。
二、TopN 可以局部取数再全局归并
如果只是查最新 20 条订单,可以让每个分片按时间倒序取 20 条,然后在应用层做一次归并排序,取全局前 20 条。这是合理的,因为全局前 20 一定来自各分片局部前 20 的集合。
但如果是深分页,局部取数数量会随着 offset 增长。越往后翻,需要从每个分片取回的数据越多。因此跨分片深分页是非常危险的查询模式。
可以把归并想成多路合并:每个分片返回一个有序列表,应用维护一个小顶堆或大顶堆,不断取出当前最靠前的记录。这个算法本身不复杂,真正的问题是候选数据规模和数据库扫描成本。
三、游标分页比 offset 分页更适合分布式场景
游标分页不是说“跳过前 10000 条”,而是说“从上一页最后一条之后继续查”。比如按 create_time desc, id desc 排序,下一页带上上一页最后的 create_time 和 id:
where (create_time < last_time)
or (create_time = last_time and id < last_id)
order by create_time desc, id desc
limit 20
这样每个分片可以利用索引继续往后扫描,不需要反复跳过大量数据。游标分页要求排序字段稳定且有唯一兜底字段,常用 create_time + id。
它的限制是不能方便地跳到任意页,但移动端信息流、订单列表、消息列表通常并不需要跳页,游标分页更符合业务体验。
四、聚合查询要尽量预计算
跨分片 count、sum、group by 也会放大。简单计数可以每个分片算局部 count,再累加;但复杂 group by 会返回大量中间结果,应用层合并压力很大。
高频统计应该预计算。例如商家订单数、用户未读数、商品销量、排行榜,可以在写入时同步或异步维护汇总表。实时性要求不高的报表可以走离线数仓或 OLAP 引擎。
不要让用户每刷新一次页面都触发跨 64 个分片的 group by。那不是查询,是给数据库做压力测试。
五、用查询约束保护系统
分库分表系统通常会限制后台查询条件:必须带时间范围、租户 ID、状态条件,最大时间跨度不能超过某个阈值,导出任务走异步。这样做不是产品偷懒,而是保护在线数据库。
如果确实有复杂查询需求,就应该建设专门的查询链路:主库通过 binlog 或消息同步到 ES/ClickHouse,用户查询分析库,交易库只负责交易。这是职责分离。
六、跨分片分页要关注正确性和资源放大
跨分片分页的难点不是写一个归并排序,而是同时保证正确性和控制资源。假设按创建时间倒序查全站订单第一页,每个分片取前 20 条再合并是正确的;但查第 1000 页时,如果用 offset,每个分片可能要取前 20020 条,64 个分片就是一百多万条候选数据。数据库扫描、网络传输、应用内存都会被放大。
更糟的是排序字段如果不唯一,会出现翻页重复或漏数据。只按 create_time 排序时,同一毫秒可能有多条记录,上一页和下一页边界不稳定。因此游标分页通常要使用组合排序键,例如 create_time + id,保证全局顺序稳定。
对于后台查询,要强制加时间范围、租户、状态等过滤条件。没有边界的跨分片分页应该改成异步导出或分析库查询。在线接口最怕“看起来只是翻页,实际扫全库”。
七、聚合查询要区分实时、准实时和离线
计数、求和、排行榜、分组统计都叫聚合,但实时性要求差别很大。购物车商品数、未读消息数可能需要较实时;月度销售报表可以分钟级或小时级;经营分析可以离线 T+1。不同实时性对应不同架构。
强实时小范围聚合可以在单分片内做,或者维护计数表。准实时聚合可以通过消息流更新 Redis、ClickHouse、Doris 等读模型。离线聚合交给数仓。不要把所有聚合都压给在线分片库,否则分库分表越多,聚合越贵。
面试追问“count 怎么做”时,不要只答“每个分片 count 再相加”。这是低频小规模可以接受的方案。高频 count 要维护汇总表或缓存;精确 count 成本高时,可以根据场景使用近似值、延迟值或异步统计。关键是把查询的实时性和准确性需求先问清楚。
八、常见误区与追问
这道题要紧扣「跨分片分页与聚合」本身回答,不能把它混成泛泛的分库分表套话。面试官通常不是只听定义,而是看你能不能把适用场景、关键流程、失败边界和工程取舍串起来,尤其要说明拆分边界、路由规则、扩容迁移、跨库查询和一致性兜底。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 跨分片分页聚合要避免全库深分页,常用分片内 TopN、游标翻页、预聚合和搜索/OLAP 系统承接 | 不要停在名词解释 |
| 流程机制 | 明确排序字段 -> 各分片查 TopN 或游标段 -> 应用层归并排序 -> 截取目标页 -> 缓存或预聚合热点结果 -> 复杂报表转 OLAP | 要说清触发点、状态变化、确认点和失败兜底 |
| 工程取舍 | 查第 100 页每页 20 条,如果 32 个分片都查 offset 2000,实际可能读取 64000 多行再合并 | 分库分表能扩展容量和吞吐,但会带来路由、事务、Join、唯一约束、扩容和运维复杂度 |
跨分片分页与聚合 面试拆解:
1. 明确排序字段
2. 各分片查 TopN 或游标段
3. 应用层归并排序
4. 截取目标页
5. 缓存或预聚合热点结果
6. 复杂报表转 OLAP
记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「跨分片分页与聚合」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。
- 误区:跨分片 limit offset 成本和单库一样。 每个分片都可能执行深分页,最后还要全局归并。
- 误区:应用层合并一定准确。 必须有稳定排序键,否则相同时间戳或并发写入会导致漏重。
- 误区:所有聚合都能在线算。 大范围 count、sum、group by 更适合预聚合、离线或 OLAP。
- 追问:如何优化深分页? 用游标、seek after、时间范围、二级索引或搜索系统。
- 追问:TopN 怎么做? 每个分片取局部 TopN,再在应用层做 k 路归并。
- 追问:总数怎么显示? 可用估算值、缓存计数、异步统计,避免每次扫全分片。
九、加强记忆
跨分片排序分页的关键词是“局部有序、全局归并、深分页放大”。跨分片聚合的关键词是“局部计算、全局合并、高频预计算”。能带分片键就别跨分片;必须跨分片就限制范围;复杂分析查询交给异构系统。