MySQL 多租户订单表架构演进:从 tenant_id 联合索引到租户分片
来源:17golang原创
时间:2026-07-02 13:34:56 259浏览 收藏
多租户系统里的订单表,早期通常会把所有租户的数据放在一张 orders 表里,再用 tenant_id 区分归属。数据量小的时候这很清爽;一旦某个大客户的订单量、查询量和导出任务明显高于其他租户,单表联合索引只能解决一部分查询成本,真正的架构问题会变成:哪些租户继续共享,哪些租户需要被路由到独立资源里。
- 多租户订单表的第一条规则,是所有核心查询都必须带上
tenant_id,并让联合索引从租户维度开始。 - 联合索引能减少扫描范围,但不能隔离一个热点租户对 CPU、IO、连接池和慢查询队列的影响。
- 当大租户长期拉高
rows、慢日志和接口延迟,就要把“加索引”升级为“租户路由 + 数据迁移 + 回读校验”。 - 分区表可以帮助部分查询裁剪无关分区,但它不是租户级资源隔离方案,主键和唯一键限制也要提前核对。
- 规模背景:一张 orders 表承载所有租户
- 原架构瓶颈:一个热点租户拖慢整张订单表
- 第一阶段:用 tenant_id 领头的联合索引稳住主查询
- 第二阶段:拆出热点租户的路由和写入链路
- 关键取舍:分区、分表和独立库分别解决什么问题
- 上线后看哪些信号
- 相关问题
- 总结
规模背景:一张 orders 表承载所有租户
先看一个常见表结构。订单表既要给后台列表查,又要给对账、导出、售后、统计任务用。早期为了开发简单,所有租户共享一张表:
CREATE TABLE orders ( id BIGINT PRIMARY KEY, tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, status TINYINT NOT NULL, amount DECIMAL(12, 2) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL );
列表查询一般长这样:
SELECT id, status, amount, created_at FROM orders WHERE tenant_id = ? AND status = ? AND created_at >= ? ORDER BY created_at DESC LIMIT 50;
这个模型的优点很明显:表少、代码简单、统计也容易写。问题也同样明显:所有租户共用同一张物理表、同一组索引、同一个实例资源。只要一个租户的数据量明显偏大,或者某个租户开启高频导出,其他租户的正常查询也可能被拖住。
原架构瓶颈:一个热点租户拖慢整张订单表
MySQL 官方文档说明,索引用于快速找到具有特定列值的行;多列索引可以服务测试索引中全部列或最左前缀列的查询。这给了我们第一层优化方向:把高频条件放进合适的联合索引里。但多租户场景的麻烦在于,热点租户并不只是“查询没走索引”,它常常是“走了索引也要读很多行”。

假设普通租户每月只有几千单,热点租户每月有几百万单。相同的 SQL、相同的索引,在普通租户上可能只扫几十行,在热点租户上却要扫大量历史记录。此时单表继续扩容会遇到几个瓶颈:
- 索引页更大,缓存命中率下降,热点租户把更多 buffer pool 空间占走。
- 大范围查询和导出任务增加磁盘读写压力,影响普通租户列表页。
- 慢查询排队会占用连接池,让应用侧看起来像“所有租户都慢”。
- 归档、修复、回放这类后台任务越来越难按租户隔离。
第一阶段:用 tenant_id 领头的联合索引稳住主查询
在没有分片之前,先把主查询的索引设计做好。对上面的订单列表,可以先建一个符合过滤和排序方向的联合索引:
CREATE INDEX idx_orders_tenant_status_created ON orders (tenant_id, status, created_at DESC);
这样做的核心不是“字段越多越好”,而是把租户边界放在最前面。多列索引有最左前缀规则,tenant_id 作为第一列,可以让同一租户内的状态和时间范围查找更集中。验证时看三类信号:
| 检查项 | 希望看到的变化 | 说明 |
|---|---|---|
EXPLAIN 的 key |
使用 idx_orders_tenant_status_created |
说明优化器选择了目标索引 |
rows |
从大范围下降到租户内较小范围 | 说明扫描范围被租户和条件收窄 |
| 慢日志 | 普通租户查询明显减少 | 说明主链路先被稳住 |
如果列表还需要按用户查,可以再根据真实查询频率补充 (tenant_id, user_id, created_at)。不要给每个接口都加一条索引,索引越多,写入、更新和空间成本也越高。
第二阶段:拆出热点租户的路由和写入链路
当联合索引已经命中,但热点租户仍然长期拉高延迟,就要从表设计进入路由设计。比较稳的做法不是一次性全量拆所有租户,而是先给热点租户建立路由表:
CREATE TABLE tenant_db_route ( tenant_id BIGINT PRIMARY KEY, route_type VARCHAR(20) NOT NULL, shard_key VARCHAR(64) NOT NULL, updated_at DATETIME NOT NULL );
应用写入订单前先查本地缓存的租户路由:普通租户继续写共享库,热点租户写独立分片。读接口也走同一套路由,避免写到新分片、读还去旧表的割裂问题。

迁移时建议按下面顺序推进:
- 先建新分片和目标表,表结构、索引、字符集和时区规则保持一致。
- 按租户维度复制历史数据,复制后比对订单数、金额合计和最大
id。 - 应用侧开启双读校验,只对目标租户生效,发现差异可以退回共享表读取。
- 切写入路由,让热点租户的新订单进入新分片。
- 观察一段时间后,再清理旧表中已经迁出的热点租户数据。
这一步的关键是“按租户渐进迁移”。如果一开始就分所有租户,很容易把路由、迁移、回滚和报表链路一起复杂化。
关键取舍:分区、分表和独立库分别解决什么问题
多租户订单表变慢时,团队常会在分区、分表、分库之间摇摆。它们解决的问题不同,不能只看名字相似。
| 方案 | 主要收益 | 适合场景 | 需要注意 |
|---|---|---|---|
| 联合索引 | 减少单次查询扫描范围 | 大多数租户查询还在可控范围内 | 无法隔离热点租户资源消耗 |
| MySQL 分区 | 在条件可裁剪时减少无关分区扫描 | 按时间或固定键管理历史数据 | 主键、唯一键和分区表达式有限制,不能当成完整分片 |
| 租户分表 | 降低单表数据量,迁移边界清晰 | 热点租户少、表结构稳定 | 报表和跨租户查询要额外聚合 |
| 独立库或独立实例 | 隔离连接、IO、缓存和维护窗口 | 大客户、强隔离、付费等级差异明显 | 运维成本、路由和备份策略都会变复杂 |
MySQL 分区裁剪的思路是:当条件能明确落到某些分区时,就不扫描不可能命中的分区。这个能力很适合按时间清理和部分范围查询,但它仍在同一个表模型里工作。官方文档也明确提到分区键与主键、唯一键之间有约束关系,做方案前要先核对现有唯一约束是否允许这样改。
上线后看哪些信号
拆分不是把数据搬走就结束。上线后至少要观察三个层面的信号:
- 查询层:热点租户迁出后,共享表主查询的
rows、慢日志次数、接口 P95 是否下降。 - 写入层:热点租户新订单是否全部进入新分片,路由缓存是否有过期和误命中。
- 运维层:备份、归档、数据修复、账单统计是否已经适配新的路由关系。
更稳的验收方式,是把迁移前后的核心 SQL 都留一份样例:
EXPLAIN SELECT id, status, amount, created_at FROM orders WHERE tenant_id = 8421 AND status = 2 AND created_at >= '2026-07-01' ORDER BY created_at DESC LIMIT 50;
如果热点租户被迁出,共享表上这类查询应该不再拖累其他租户;新分片上的查询则要单独看索引、归档和限流策略。分片后的性能治理不是结束,而是把“全局混在一起慢”改成“按租户定位和治理”。
相关问题
多租户表一定要用 tenant_id 做联合索引第一列吗?
大多数按租户隔离的业务查询都应该这样做。只要接口天然属于某个租户,tenant_id 放在联合索引前面可以先收窄租户范围,再按状态、时间或用户继续过滤。
热点租户出现后,应该先分表还是先独立库?
先看瓶颈在哪里。如果只是单表过大,分表可能够用;如果连接、缓存、IO 和维护窗口都被大租户占用,独立库或独立实例更符合隔离目标。
MySQL 分区能不能替代租户分片?
通常不能。分区可以帮助管理和裁剪部分查询范围,但它不等于资源隔离,也不能替代应用层路由。多租户隔离通常还要考虑连接池、备份、权限、账单和运维边界。
租户迁移时最怕什么问题?
最怕写入和读取路由不一致。建议先做历史数据校验,再做双读或抽样回读,最后切写入路由,并保留可退回共享表的开关。
总结
MySQL 多租户订单表的演进,不是一上来就分库分表。更可靠的路线是:先保证所有主查询带 tenant_id,用租户维度领头的联合索引压低普通查询成本;当热点租户继续制造高扫描、高延迟和队列压力,再通过租户路由把它迁到独立分片。这样既保留早期单表的简单性,也给大客户和高峰流量留出清晰的扩展路径。
-
374 收藏
-
499 收藏
-
384 收藏
-
184 收藏
-
265 收藏
-
273 收藏
-
数据库 · MySQL | 5小时前 | MySQL · 查询优化 · 统计信息 · 性能排查 · 执行计划 EXPLAIN ANALYZE MySQL 8.0 直方图统计 ANALYZE TABLE420 收藏
-
334 收藏
-
401 收藏
-
312 收藏
-
471 收藏
-
499 收藏
-
382 收藏
-
262 收藏
-
数据库 · MySQL | 5天前 | MySQL · 权限管理 · 备份 · mysqldump · 数据库安全 · 最小权限 mysqldump备份账号 MySQL角色 partial_revokes 备份权限413 收藏
-
278 收藏
-
数据库 · MySQL | 6天前 | MySQL · JSON · 索引 · 数据库 · 查询优化 · 生成列 · json_extract 索引优化 列表筛选 生成列 MySQL JSON JSON索引351 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习