很多SaaS后台在数据量较小时,使用LIMIT 0,20、LIMIT 1000,20甚至LIMIT 100000,20都没有明显感觉。随着订单、用户、日志、工单等数据不断增加,深分页开始出现明显的延迟。用户翻到第几千页以后,页面可能从几十毫秒变成几百毫秒甚至几秒。
这通常不是MySQL突然“变慢”了,而是传统OFFSET分页的工作方式决定了它在深分页场景下成本会不断增加。
例如查询:
SELECT id, created_at, status
FROM orders
WHERE tenant_id = 1001
ORDER BY id
LIMIT 100000, 20;
数据库需要找到满足条件并按照指定顺序排列的记录,然后跳过前面的100000条,最后返回20条。即使查询能够利用索引,数据库仍然可能需要扫描大量索引记录才能到达真正需要返回的位置。
因此,深分页真正的问题不是“LIMIT本身很慢”,而是OFFSET越大,需要处理并丢弃的前置记录越多。它通常表现为扫描行数增加、CPU和I/O压力增加、查询延迟上升,而不是简单的“指数级增长”。
如果后台经常出现这种查询,第一步不是马上上Redis、Elasticsearch或者分库分表,而是先检查查询条件、排序方式和索引是否匹配。
对于典型SaaS多租户系统,例如:
SELECT id, created_at, status
FROM orders
WHERE tenant_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 20;
如果这是高频查询,一个常见的索引设计是:
CREATE INDEX idx_orders_tenant_created_id
ON orders (tenant_id, created_at, id);
这样数据库可以先限定租户,再按照排序字段读取数据。索引是否真正有效,还应该通过EXPLAIN或者MySQL 8中的EXPLAIN ANALYZE验证,而不是看到一个索引就认为问题已经解决。
索引字段顺序同样重要。SaaS系统通常首先需要解决租户隔离,因此大量查询都会带有tenant_id。如果业务模式是“一个租户查询自己的订单,再按照创建时间排序”,那么(tenant_id, created_at, id)往往比单独建立created_at索引更符合实际访问路径。
对于返回大量列表字段的查询,还可以考虑覆盖索引,但不能为了“避免回表”就把几十个业务字段全部塞进索引。索引越宽,写入、更新、缓存空间和维护成本也越高。最终应该根据真实查询字段和EXPLAIN ANALYZE结果决定。
当OFFSET开始成为主要成本时,更适合SaaS后台的办法通常是Keyset Pagination,也就是通常所说的Cursor Pagination。这里的“Cursor”不是MySQL内部的数据库游标,而是一种利用上一页最后一条记录作为下一页边界的分页方式。
例如按照递增ID查询:
SELECT id, created_at, status
FROM orders
WHERE tenant_id = ?
AND id > ?
ORDER BY id ASC
LIMIT 20;
上一页最后一条记录的ID就是下一页的游标。数据库不需要从第100000条开始数,而是可以直接从索引中的这个位置继续读取20条记录。
如果后台按照时间倒序排列,则应该让游标与排序字段保持一致。例如:
SELECT id, created_at, status
FROM orders
WHERE tenant_id = ?
AND (
created_at < ?
OR (created_at = ? AND id < ?)
)
ORDER BY created_at DESC, id DESC
LIMIT 20;
相应的索引可以设计为:
CREATE INDEX idx_orders_tenant_created_id
ON orders (tenant_id, created_at, id);
这里为什么还需要id?因为created_at可能相同。单独使用时间作为游标,在大量记录拥有相同时间戳时可能出现重复或者漏记录。把created_at和唯一性更好的id组合起来,可以形成稳定的排序边界。
Keyset Pagination非常适合订单列表、消息列表、操作日志、工单、交易记录以及无限滚动等场景。但它也有明显限制:如果用户要求“直接跳到第500页”,Cursor分页并不天然适合。传统OFFSET在这种需要任意页码跳转的后台报表中仍然有使用价值,只是不能让它无限深入。
因此,实际SaaS系统往往采用混合策略。首页和前几页使用普通LIMIT分页并没有问题;当业务不需要显示总页数、用户主要进行下一页或上一页浏览时,可以逐步采用Keyset Pagination;如果必须支持任意页码跳转,则应该重新评估这个页面是否真的需要传统分页。
缓存也不能简单理解为“把分页结果全部放进Redis”。分页结果高度依赖租户、筛选条件、排序方式和数据更新时间。用户随便修改一个筛选条件,就可能产生完全不同的缓存Key。如果数据变化频繁,大量分页缓存还会带来失效和内存管理问题。
Redis更适合缓存真正稳定、访问频率高、计算成本明显的数据,而不是机械地缓存所有分页结果。对于首页统计、热门数据、权限信息、配置数据等场景,缓存通常更加容易获得收益。对于实时变化的订单列表,首先应该把SQL和索引做好。
SaaS系统还需要特别注意租户隔离。一个查询如果理论上只应该看到当前租户的数据,那么tenant_id不能只是应用层“记得加”的一个条件,而应该成为整个查询、索引和权限设计的一部分。
例如:
SELECT id, created_at, amount
FROM orders
WHERE tenant_id = ?
AND status = ?
ORDER BY created_at DESC, id DESC
LIMIT 50;
对应的索引可能是:
CREATE INDEX idx_orders_tenant_status_created_id
ON orders (tenant_id, status, created_at, id);
具体字段顺序仍然要根据真实查询模式决定。不要因为网上某个案例使用了四列联合索引,就照搬到自己的数据库。
至于“每个租户一个Schema”或者“按照租户进行数据库分区”,也不能作为SaaS分页优化的标准答案。大量租户的小数据场景下,统一表加tenant_id通常更容易管理;只有当租户规模、数据隔离、备份恢复、合规要求或者数据分布已经达到一定程度时,才需要重新考虑Schema、数据库实例、分区或者分片。
读写分离同样不是解决深分页的直接办法。如果SQL本身需要扫描大量索引记录,把它从主库移动到只读副本,只是把压力转移了。更重要的是,复制存在延迟时,用户刚写入的数据可能暂时在读库中看不到。因此读写分离应该服务于整体数据库负载,而不是作为LIMIT分页慢的第一解决方案。
Elasticsearch或OpenSearch也不应该因为“数据超过百万行”就自动加入系统。MySQL本身完全可以处理大量结构化列表查询,关键取决于数据规模、查询模式、索引设计、并发量以及硬件。只有当业务真正需要全文搜索、复杂文本相关性、模糊检索、聚合搜索或者跨字段搜索,并且MySQL已经无法经济地承担这些查询时,搜索引擎才有明确价值。
同样,分区表、分库分表、TiDB等分布式数据库也不是深分页的直接药方。如果一个查询本身没有合适索引,那么把数据库变成分布式系统以后,问题甚至可能变得更加复杂。
实际优化时,建议先打开慢查询日志,观察真实SQL,再使用EXPLAIN和EXPLAIN ANALYZE查看执行计划。重点关注访问类型、实际扫描行数、返回行数、排序、临时表以及实际执行时间。
如果一个页面只需要20条记录,却扫描几十万甚至几百万条索引记录,那么问题已经非常清楚。此时应该优先检查WHERE条件、ORDER BY、联合索引以及分页方式,而不是先增加缓存服务器。
压力测试也应该模拟真实访问模式。不要只测试第一页,因为第一页通常恰恰是最容易查询的。应该同时测试第一页、第100页、第1000页以及典型的Cursor位置,并观察P50、P95和P99延迟,同时记录CPU、磁盘I/O、Buffer Pool命中情况、数据库连接数以及扫描行数。
对于一个成熟的SaaS后台,分页优化通常可以形成一个比较清晰的演进路径:先保证租户条件正确,再让WHERE和ORDER BY与索引匹配;随后使用EXPLAIN ANALYZE验证执行计划;当深分页成为实际瓶颈时,把适合的列表改成Keyset Pagination;只有在真实负载证明需要时,再考虑Redis、读写分离、搜索引擎、分区或者分布式数据库。
分页技术没有一个适用于所有SaaS系统的“标准答案”。数据量只有几万行、查询并发很低的后台,没有必要为了避免OFFSET而把系统搞成复杂的分布式架构;而每天产生数百万条订单或日志、用户又频繁翻阅历史数据的系统,则应该尽早让分页方式与索引结构一起演进。
真正需要优化的并不是LIMIT这个SQL关键字,而是数据库为了找到这一页数据到底做了多少无效工作。让索引直接把数据库带到目标位置,再让查询只读取真正需要的数据,通常比不断增加缓存、服务器和中间件更加直接。
分享 Facebook |
X |
Whatsapp |
Linkedin
捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP
责任编辑:雨轩 来源:中国观察 转载请注明作者、出处並保持完整。
今日热点:
近期焦点:
推荐阅读: