滚动新闻 →
Kubernetes 为什么逐渐进入 AI 基础设施,传统虚拟机架构有哪些不足 委内瑞拉反对派领袖马查多结束流亡回国 加拿大卑诗省政坛格局巨变 保守党时隔近百年重返执政 五中全会前胡春华突“失踪” 内部人士曝真实内情 川普密西根集会:力阻穆斯林候选人参议院席位 黑海安全成焦点 保加利亚大选聚焦对俄立场 Google Cloud Storage 成本怎么优化,生命周期和存储类别应该如何组合 MySQL 游标分页怎么实现?适合 SaaS 大数据列表的实际场景 佛罗里达农工大学附近发生枪击案 至少12人受伤 USB设备插入以后电脑立即重启,可能存在什么硬件故障 Windows 11 PIN 登录怎么设置?它和传统密码有哪些实际区别? MySQL root 密码忘记了怎么办?开发环境中的恢复思路和注意事项 古诗中的秋天为什么常常带有不同情绪 问界又烧了!一周连烧3辆 网讽“移动火化炉” 蔡康永深夜澄清未支持台独 暗含离别之意 2026年最大明亮普通满月‘猎人月’周日晚现身夜空 传统中医如何理解“食养” 宣纸为什么适合中国书画 孩子看到别人优秀就不高兴怎么办 长时间拍视频应该如何管理存储 美国对30所高校涉外研究项目实施安全审查 五中全会前地方官场大洗牌 派系博弈暗流涌动 吃蘑菇后见到“小人国” 研究:两地蘑菇竟同种 明朝为什么高度重视中央集权 家具重新摆放真的可以改变居住体验吗 财新揭露北京高官与女商人关系遭紧急删除 北京老龄化加速 养老压力提前显现 中共五中全会前维权活动遭全面监控 重点城市出现大规模执法行动 米粉和面条背后有什么不同的农业基础 当好莱坞资本全面拥抱AI电影 工业革命将如何重塑全球影视产业 匈牙利香肠节:百年传统技艺的现代演绎 巴西总统大选第二轮投票:卢拉与博索纳罗激烈角逐 生活成本高企引发多国抗议潮 意西比阿民众周末示威 美前高官:中共体制矛盾重重 难逃失败命运 婚姻顾问支招:如何避免夫妻情感疏离 奥迪计划投资42亿美元在美国乔治亚州建设新工厂 美专家警告:中共云端战威胁需加速AI基建应对 欧洲多国暴力抗议升级 军警全面部署应对 海风吹过灯塔,加拿大卢嫩堡的彩色房屋依然像童话 2026赛季韩国女子围棋联赛积分榜(大结局)

MySQL 游标分页怎么实现?适合 SaaS 大数据列表的实际场景

发布时间: 2026-10-25 17:45:01    最后更新: 2026-10-25 18:43:59    阅读:10  约12 分钟阅读     

MySQL 游标分页怎么实现?适合 SaaS 大数据列表的实际场景
图片说明:示意图   图片来源:Public Domain(公有领域)
在SaaS系统中,订单、用户、操作日志、消息记录等列表经常会随着业务增长达到百万甚至千万级。最初使用 LIMIT 0, 100 看起来完全没有问题,但当用户翻到很深的页面时,LIMIT 500000, 100 这样的查询就可能产生越来越高的扫描和丢弃成本。

这也是游标分页逐渐被采用的原因。这里所说的Cursor Pagination,并不是MySQL存储过程里的数据库游标,而是一种基于排序键继续向后取数据的分页方式,也常被称为Keyset Pagination。它不要求数据库先找到前面几十万条记录再跳过去,而是直接从上一页最后一条记录之后开始读取。

最简单的例子是订单表按照递增的order_id排列:

SELECT
order_id,
customer_id,
amount,
status,
created_at
FROM orders
WHERE tenant_id = ?
AND order_id > ?
ORDER BY order_id ASC
LIMIT 100;

第一次查询没有游标,可以从最小的order_id开始;返回100条以后,服务端把最后一条记录的order_id作为下一页的cursor。下一次请求携带这个值,数据库直接查找order_id更大的记录。

真正决定这种查询性能的不是SQL看起来有多短,而是索引是否与查询条件和排序顺序匹配。对于典型的SaaS多租户表,更有价值的索引通常是:

CREATE INDEX idx_orders_tenant_order
ON orders (tenant_id, order_id);

这样MySQL可以先定位某个租户的数据范围,再按照order_id的索引顺序继续读取。仅仅给tenant_id建立索引,或者单独给order_id建立索引,都可能无法达到联合索引带来的效果。

尤其要注意,tenant_id通常不应该建立唯一索引。一个租户显然需要拥有大量订单,因此正确的结构一般是普通联合索引,而不是:

UNIQUE INDEX (tenant_id)

如果需要保证订单编号在租户内部唯一,可以根据业务设计使用:

UNIQUE INDEX (tenant_id, order_id)

但如果order_id本身已经是全局唯一主键,则是否需要这个唯一联合索引,要根据实际查询和写入模式判断,不能为了分页盲目增加索引。

游标字段必须能够稳定地表达分页顺序。最理想的情况是使用唯一且单调的键,例如自增ID。如果业务要求按照创建时间排序,就不能简单使用created_at一个字段,因为多个订单完全可能拥有相同的时间戳。这时应该使用复合游标,例如:

SELECT
order_id,
customer_id,
amount,
created_at
FROM orders
WHERE tenant_id = ?
AND (
created_at < ?
OR (created_at = ? AND order_id < ?)
)
ORDER BY created_at DESC, order_id DESC
LIMIT 100;

对应的索引则可以设计为:

CREATE INDEX idx_orders_tenant_created_id
ON orders (tenant_id, created_at, order_id);

这里的order_id承担了最终的唯一排序作用。这样即使100条订单拥有完全相同的created_at,分页仍然能够确定下一条记录从哪里开始。

这也是游标分页最容易出错的地方之一。很多系统写成:

ORDER BY created_at DESC
LIMIT 100;

然后把最后一条记录的created_at作为下一页游标。如果大量记录拥有相同时间戳,就可能出现重复或者遗漏。真正可靠的游标应该覆盖完整的排序键,而不是只保存其中一个字段。

数据发生变化时,也需要区分不同场景。假设用户正在浏览订单列表,此时系统新增了一批订单。如果按照order_id ASC向后翻页,新订单通常不会影响已经读取过的位置;如果用户删除了一条尚未读取的记录,也只是下一页少了一条数据,不会要求数据库找到这个已经不存在的ID。因此,游标本身并不需要“指向一个仍然存在的记录”,它只是一个边界值。

但如果业务要求用户在整个分页过程中看到一个固定的数据快照,普通游标分页仍然不够。这时需要考虑事务快照、版本号、固定查询时间点或者其他一致性方案。单纯增加一个version字段,并不能自动把动态数据变成一致性快照。

SaaS系统还有一个非常现实的问题,就是租户隔离。分页查询必须把租户条件作为查询逻辑的一部分,而不能先根据游标查询大量数据,再在应用层过滤租户。典型查询应该始终类似:

WHERE tenant_id = ?
AND order_id > ?

这既关系到性能,也关系到数据隔离。如果系统使用共享数据库、共享表的多租户架构,tenant_id通常应该出现在主要业务查询的联合索引设计中。

对于倒序列表,例如后台订单通常希望“最新订单在最前面”,则可以使用:

SELECT
order_id,
amount,
status,
created_at
FROM orders
WHERE tenant_id = ?
AND order_id < ?
ORDER BY order_id DESC
LIMIT 100;

第一次查询不需要cursor,之后把当前页面最后一条记录的order_id传给下一次请求即可。

应用层最好不要直接把数据库字段全部暴露成cursor。更成熟的API可以把游标编码成一个不透明字符串,例如Base64编码的JSON,里面保存排序键、租户上下文以及必要的版本信息,并由服务端进行签名或校验。这样前端不需要理解数据库内部结构,也可以避免用户随意修改cursor造成异常查询。

例如API可以返回:

{
"data": [
{
"order_id": 1001,
"amount": 89.50
}
],
"next_cursor": "eyJvcmRlcl9pZCI6MTAwMX0=",
"has_more": true
}

下一次请求只需要携带next_cursor即可。对于很多SaaS列表,这比让前端保存一个last_order_id更加容易维护。

PHP PDO也应该使用参数化查询,但需要注意LIMIT参数的处理方式。为了避免不同驱动对LIMIT占位符处理产生兼容性问题,可以先在服务端把page size转换为整数,并限制其范围,然后将它安全地放入SQL。例如:

$pageSize = max(1, min((int)$pageSize, 500));

$sql = "
SELECT order_id, customer_id, amount, status, created_at
FROM orders
WHERE tenant_id = ?
AND order_id > ?
ORDER BY order_id ASC
LIMIT $pageSize
";

$stmt = $pdo->prepare($sql);
$stmt->execute([$tenantId, $lastOrderId]);

这里的关键不是“LIMIT一定不能使用参数”,而是不要把未经验证的用户输入直接拼进SQL。pageSize必须在服务端转换成整数并限制范围。

游标分页也并非所有场景都比OFFSET分页好。用户如果需要直接跳到第100页、第500页,或者页面必须显示“第几页/共多少页”,传统分页往往更加符合产品需求。游标分页最适合的是“下一页”“加载更多”“无限滚动”以及后台不断向后读取大量数据等场景。

此外,游标分页通常不适合依赖随机访问的报表界面。对于分析系统,如果用户需要按照多个条件筛选、排序并跳转到任意位置,真正需要解决的可能是查询模型、索引、汇总表甚至分析型数据库,而不是简单把OFFSET换成cursor。

至于覆盖索引,也不能理解为“把所有SELECT字段都塞进索引”。如果一个订单列表需要返回十几个甚至几十个字段,把这些字段全部加入索引会显著增加索引体积、写入成本和Buffer Pool压力。更合理的方式是先根据真实查询选择索引键,再通过EXPLAIN ANALYZE观察实际执行计划和回表成本。

分区表同样不能因为数据量大就自动启用。MySQL分区有明确的分区键、查询条件和运维限制。对于很多SaaS系统,一个设计良好的(tenant_id, created_at, id)或类似联合索引,可能已经足够应付大量列表查询。是否需要分区,应该根据数据规模、查询模式、生命周期管理和实际IO压力决定。

读写分离也不是游标分页的必要条件。如果主库完全能够承受列表查询,就没有必要为了分页专门引入从库。引入读副本以后,还必须考虑复制延迟:用户刚刚创建一笔订单,紧接着从从库查询列表时,可能暂时看不到这条记录。对于对实时性要求较高的后台系统,这种行为需要在应用层明确处理。

另外,MySQL 8.0已经移除了Query Cache,因此不能再把“开启Query Cache”作为现代MySQL分页优化方案。真正应该关注的是正确的索引、执行计划、Buffer Pool、IO能力、查询返回字段数量以及实际并发量。EXPLAIN ANALYZE、慢查询日志和Performance Schema比依赖已经不存在的Query Cache更加重要。

最后,还需要重新理解“百万级数据必须使用游标分页”这种说法。数据量本身并不是唯一决定因素。一张拥有数千万条记录、但查询始终只访问最近数据并且索引设计合理的表,可能运行得很好;反过来,一张只有几十万条数据、却频繁执行复杂排序和深OFFSET查询的表,也可能出现明显性能问题。

判断分页方案最可靠的方法,是观察真实的查询延迟、扫描行数、IO、CPU、锁等待和并发情况。可以先用EXPLAIN ANALYZE比较不同分页深度的实际执行成本,再决定是否需要从OFFSET分页迁移到Keyset Pagination。

对于典型的SaaS订单列表,比较成熟的结构通常是:使用稳定且唯一的排序键,建立与租户过滤和排序顺序匹配的联合索引,服务端生成并校验不透明cursor,API采用next_cursor和has_more方式向前端提供数据,同时根据业务要求处理动态数据的一致性。

游标分页真正解决的并不是“数据库数据很多”这么简单,而是避免深度OFFSET查询随着分页位置增加而产生越来越大的扫描成本。它也不是一个可以脱离索引独立存在的优化技巧。只有查询条件、排序字段、联合索引和API分页协议共同设计,SaaS的大数据列表才能真正做到稳定、高效,而且随着数据规模增长仍然保持可控。

喜欢这篇报道?

使用下面的功能,方便以后继续阅读和分享 MNewsTV

设为 Google 新闻首选来源 让 Google 新闻优先显示 MNewsTV 的最新报道 ›
★ 我的收藏 查看已经收藏的文章
关于文章收藏 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。
分享这篇报道

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP