线上出了个离谱问题:运营同学在后台导出近 3 个月订单时,三个巧技点击分页到第 100 页,让分直接把数据库查崩了。库分排查后发现,翻页代码里写了 LIMIT 9900,直接 100,在分库分表场景下,拉满这行 SQL 相当于让 8 个分片各查 1 万条数据,三个巧技再拉到应用层内存排序,让分直接把内存溢出了。库分
其实分库分表的翻页分页查询,藏着很多反常识的直接奇技淫巧,但这些技巧都有严格的拉满适用边界,用错了反而会埋坑。三个巧技我来分享 3 个实战中验证过的让分骚操作。

单表分页用 LIMIT offset, size 没问题,但分库分表后,数据散在多个分片里,比如你要查 LIMIT 10000, 10(第 1001 页),会发生两件离谱的事:
全分片扫描:每个分片都要执行 LIMIT 0, 10010(因为不知道其他分片的数据分布,只能把前 10010 条都查出来,避免漏数据);
内存爆炸排序:假设 8 个分片,每个返回 10010 条,共 8 万多条数据,全拉到应用层排序,再截取第 10000-10010 条。内存和 CPU 直接飙红,我之前见过最夸张的亿华云案例,offset=100000 时,一个分页请求耗时 12 秒,直接触发服务熔断。
所以我们下边要讲的都是绕开全分片扫描 + 内存排序,但每个方案的适用场景天差地别,核心原则:不盲目追高性能,先看业务场景是否匹配。
锚点分页,性能最优,但仅限加载更多场景。
这是我最常用的技巧,核心思路是用数据本身的有序字段当锚点,替代 offset,比如按自增 ID 或时间戳分页。但注意:不是所有有序字段都能用,必须满足分片内 + 分片间都有序。
假设订单表按 ID 范围分 3 个分片:
分片 1:ID 1-10000(分片内有序,且小于分片 2 的 ID);分片 2:ID 10001-20000(同理);分片 3:ID 20001-30000(同理);要查第 2 页(10 条 / 页),步骤如下:
查第 1 页时:执行 ORDER BY id DESC LIMIT 10,拿到最后一条数据的 ID 是 last_id=100(这个 ID 就是云南idc服务商锚点);查第 2 页时:直接用 WHERE id < 100 ORDER BY id DESC LIMIT 10;查第 3 页时:再用第 2 页最后一条的 ID(比如 90)当锚点,执行 WHERE id < 90 ORDER BY id DESC LIMIT 10。每个分片都能独立执行 WHERE id < xxx LIMIT 10,只返回 10 条数据(不用查前 N 条)。比如查第 1001 页,每个分片也只返回 10 条,汇总后排序取 10 条,网络和内存开销直接降为原来的 1/1000。
刚才的站群服务器锚点分页不支持跳页,但有些场景比如后台管理系统,又必须要跳页,怎么办?我之前在电商后台做订单导出时,用过分片标记法,核心是给每个库、表记录数据范围和总量,快速定位目标页在哪个分片。
分片标记法支持跳页,但必须控制元数据一致性。
假设订单表按用户 ID 范围分 2 库,每库按时间分 12 表(如库 1 - 表 202401、库 1 - 表 202402...),先在 Redis 里维护 库、表级别的元数据:
库 - 表
起始 ID
结束 ID
数据总量
库 1 - 表 202401
1
5000
5000
库 1 - 表 202402
5001
12000
7000
库 2 - 表 202401
12001
18000
6000
....
....
..18000
6000
现在要查 LIMIT 15000, 10(第 1501 页),步骤如下:
查元数据定位库、表:计算累计数据量,库 1 - 表 202401(5000)+ 库 1 - 表 202402(7000)= 12000 <15000,再加上库 2 - 表 202401 的 6000,累计 18000>15000,所以目标在库 2 - 表 202401;计算表内偏移量:表内偏移量 = 15000 - 12000 = 3000,所以库 2 - 表 202401 执行 LIMIT 3000, 10;直接返回结果:因为库、表按 ID 有序,查询结果就是全局第 15000-15010 条,不用汇总其他分片。1.元数据必须实时但不能强同步:
数据新增、删除时,要同步更新 Redis 元数据,但高并发下不能加分布式锁(会卡住业务),建议用定时 + 增量日志:每 5 分钟全量统计一次,同时记录增量(如新增 100 条、删除 10 条),查询时叠加增量;
若允许最终一致性(如后台查询允许误差 10 条),这个方案很稳;若要强一致,只能放弃跳页,用锚点分页;
2.不支持非分片键排序:如果要按支付时间排序(分片键是用户 ID),支付时间在 “库 - 表” 内无序,元数据无法定位,仍需全分片扫描。
反向分页,仅适用于查最后 1 页,别乱用
这个技巧最反常识,但局限性也最大。如果要查最后几页数据(比如用户查最早的订单),用普通分页会查 LIMIT 9990, 10,但可以反向查,避开大 offset。
假设订单表按 ID 范围分片,总数据量 10000 条(1000 页,10 条 / 页),要查最后 1 页(ID 9991-10000):
反向查锚点:执行 ORDER BY id ASC LIMIT 10,拿到最前面 10 条的 ID(1-10),取最大 ID 作为反向锚点(10);查最后 1 页:执行 WHERE id > 10 ORDER BY id DESC LIMIT 10,拿到的就是 ID 10000-9991(最后 10 条);调整顺序:如果需要正序展示,把结果再倒过来即可。因为 LIMIT 0, 10 比 LIMIT 9990, 10 快 100 倍. 每个分片查前 10 条数据,汇总后取最大的 10 个 ID 作为锚点,再查大于锚点的数据,避免了大 offset 扫描。
其实分库分表分页的核心不是炫技,而是在业务和技术之间找平衡。能通过产品设计规避跳页(用加载更多),就优先用锚点分页(性能最优),必须跳页就用分片标记法(接受最终一致性);实在没办法才考虑中间件(如 ShardingSphere 的全局排序)。
(责任编辑:IT科技)