SaaS后台出现几十万甚至上百万条订单,并不意味着数据库马上就需要分库分表。对于订单管理系统来说,真正容易把查询拖慢的,往往不是数据总量本身,而是没有针对实际查询模式设计索引、使用深度OFFSET分页、排序字段不稳定,以及搜索条件与索引结构完全不匹配。MySQL官方文档也明确指出,索引能够显著加快WHERE条件和排序相关查询,但索引过多同样会增加存储以及写入维护成本,因此应该围绕真实业务查询建立有限而有效的索引。
订单后台最常见的查询通常包括订单号、客户、订单状态、创建时间以及金额等条件。订单号属于高选择性字段,通常适合建立唯一索引或普通索引;如果后台经常按照“租户+客户+创建时间”查询订单,那么更值得考虑的是组合索引,而不是给每个字段分别建立一个索引。复合索引的列顺序非常重要。以MySQL为例, (tenant_id, customer_id, created_at, id) 与 (created_at, tenant_id, customer_id, id) 并不是等价的索引,它们能够有效支持的查询模式不同。MySQL的复合索引遵循左前缀原则,因此索引设计必须从实际WHERE条件和ORDER BY组合反推。
订单列表通常还需要固定排序。例如后台默认按照最新订单排列,可以使用 created_at DESC, id DESC。这里把 id 加入排序非常重要,因为创建时间并不一定唯一。如果只使用 created_at 排序,当多个订单具有相同时间戳时,不同页面之间可能出现重复或者遗漏。一个稳定的排序键应该能够最终把每一行唯一确定下来。
分页方式也应该根据使用场景选择。传统的 LIMIT offset, page_size 非常直观,适合第一页、第二页等浅分页,也适合用户确实需要跳转到指定页码的后台界面。但是随着OFFSET不断增大,数据库仍然需要处理前面已经跳过的记录。以SQL数据库为例,大OFFSET可能导致越来越多的行被计算和跳过,因此“几十万条数据使用OFFSET一定没问题”并不是可靠的判断标准。
对于订单流、操作日志、消息列表等主要按照时间顺序向后翻页的场景,更适合使用Keyset Pagination,也就是通常所说的游标分页。假设订单按照 created_at DESC, id DESC 排序,那么下一页不能简单写成 order_id > 某个值,而应该让游标条件与排序字段对应。例如上一页最后一条记录是 created_at = '2026-09-20 10:00:00'、id = 123456,下一页可以按照相同排序方向继续查询:
WHERE (created_at, id) < ('2026-09-20 10:00:00', 123456) ORDER BY created_at DESC, id DESC LIMIT 100
实际SQL写法还需要根据数据库版本、索引和执行计划进行验证。核心思想是让数据库直接从上一次的位置继续读取,而不是重新扫描并丢弃前面大量记录。
这种方式还有一个实际优势:后台用户连续点击“下一页”时,不需要真的知道自己位于第几页。游标本质上记录的是上一页最后一条数据的位置。因此对于无限滚动、时间线和订单流水等场景,它通常比深度OFFSET更加自然。
但游标分页也有代价。用户如果要求“跳到第5000页”,游标本身并不能像OFFSET一样直接定位到一个明确页码。因此后台系统可以采用混合模式:前面少量页面允许页码分页,连续浏览使用游标;如果业务强制要求任意页码跳转,则应该重新评估查询模型,而不是简单地把OFFSET包装成所谓的高性能分页。
搜索同样需要区分类型。订单号、客户ID、状态和日期属于结构化条件,首先应该使用关系型数据库自身的索引能力。比如订单号精确查询没有必要为了几十万条记录就引入Elasticsearch。MySQL本身就支持B-Tree以及FULLTEXT等索引机制,是否需要全文搜索取决于字段内容和搜索行为,而不是单纯取决于订单数量。
如果后台要求搜索客户备注、商品描述等大量文本,并且需要分词、相关性排序、模糊搜索或者跨多个字段组合搜索,才更适合考虑专门的搜索引擎。此时应该把搜索系统看成数据库之外的查询层,而不是简单地把所有订单数据复制过去。搜索结果最终还需要回到数据库确认权限、订单状态和最新数据,尤其是在多租户SaaS系统中,不能因为搜索索引返回了一条记录就直接把订单内容展示给用户。
多租户是订单系统设计中另一个必须考虑的问题。每条订单通常应该带有 tenant_id,所有后台查询都必须在权限层和数据库查询层同时落实租户隔离,而不能依靠前端传入一个客户编号就认为数据已经隔离。对于常见的租户订单列表,可以考虑类似 (tenant_id, created_at, id) 的复合索引;如果还有客户筛选,则可能需要 (tenant_id, customer_id, created_at, id)。究竟采用哪一个,应当根据真实查询比例、选择性和执行计划决定,而不是机械地给每种条件都建立一个索引。
还有一个容易被忽略的问题是COUNT。很多后台页面会在每次打开列表时执行 SELECT COUNT(*),同时再执行一条订单列表查询。如果系统有几十万甚至数百万订单,而且筛选条件复杂,COUNT本身可能成为主要负担。对于用户只是需要“下一页”功能的列表,没有必要每次都精确计算总页数。可以改成查询 page_size + 1 条记录,用多出来的一条判断是否存在下一页。只有业务确实需要显示“共多少条”时,才执行精确COUNT,或者使用经过维护的统计数据。
订单列表还应该避免 SELECT *。后台列表通常只需要订单号、客户、状态、金额、创建时间等十几个字段,却可能把地址、商品明细、支付信息、备注甚至JSON字段一起读取。列表查询只返回必要字段,可以明显减少磁盘读取、网络传输和应用层反序列化成本。用户点击某一订单进入详情页时,再单独读取完整订单信息。
对于订单明细这种一对多数据,也不应该在列表查询中直接JOIN所有商品明细,否则很容易产生一条订单对应多行结果,随后再进行DISTINCT、GROUP BY或者应用层去重,分页就会变得复杂。更合理的做法通常是先分页得到订单主表记录,再根据这一批订单ID批量读取必要的关联数据。
缓存也不能成为数据库优化的第一反应。订单状态、支付状态、发货状态和权限信息都会变化,动态列表缓存很容易产生失效和一致性问题。真正高频、变化较少的数据,例如状态字典、地区列表或者权限配置,更适合进入Redis。对于实时订单列表,首先应该把SQL、索引和执行计划做好,再根据监控数据决定是否缓存。
几十万订单也通常没有必要立即分库分表。单表几十万行对于MySQL、PostgreSQL等成熟关系型数据库来说并不是一个天然需要分片的数量级。真正需要考虑分库分表的信号包括数据规模持续增长、单库存储和I/O压力、写入吞吐、备份恢复时间、单表索引膨胀,以及业务已经出现明显的容量边界。如果只是为了让一个几十万行的订单表“看起来更专业”而提前分片,反而会增加跨分片查询、事务、运维和数据迁移的复杂度。
同样,读写分离也不是订单分页的必选项。如果单库通过正确的索引已经可以稳定处理后台查询,就没有必要为了几十万条订单增加数据库副本。读副本真正有价值的场景,是主库已经承受明显的读压力,并且业务能够接受复制延迟。否则,系统复杂度增加了,用户看到的订单状态却可能因为复制延迟而暂时落后。
性能优化最后还是要回到真实执行计划。对于慢查询,应该使用 EXPLAIN 或数据库提供的分析工具检查实际访问路径,观察是否命中预期索引、扫描了多少行、是否出现不必要的排序或临时结果。MySQL官方文档也建议通过EXPLAIN验证索引使用情况,而不是凭感觉不断增加索引。
一个成熟的SaaS订单后台,通常可以按照这样的顺序建设:首先保证租户隔离和正确的权限过滤,然后根据真实查询建立少量高价值索引;列表采用稳定的唯一排序;浅分页可以使用LIMIT/OFFSET,连续浏览则考虑Keyset Pagination;精确订单号和结构化条件留在关系型数据库中,真正需要全文检索时再引入搜索引擎;列表只读取必要字段,COUNT根据业务需求决定是否执行;最后通过慢查询、执行计划、P95/P99延迟和数据库CPU、I/O等指标持续观察。
真正决定几十万条订单能不能跑得快的,并不是“用了Redis、Elasticsearch、分库分表”这些技术名词,而是数据库能不能沿着正确的索引快速找到下一批需要返回的订单。对于SaaS后台来说,先把查询模型和索引设计正确,再随着真实负载逐步增加缓存、读副本、搜索服务甚至分片,通常比一开始就堆叠一整套分布式架构更加可靠。
SaaS 后台有几十万条订单,搜索和分页应该怎样设计
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP