多租户场景下联合索引必须以tenant_id为最左字段,否则无法高效过滤租户数据;后续字段应按查询中where和order by的实际组合顺序排列,遵循最左前缀匹配原则;覆盖索引需权衡读写性能,避免纳入大字段;分区表不能替代联合索引,二者应协同使用。

联合索引必须包含租户ID字段
多租户场景下,tenant_id 是查询的强制过滤条件,几乎所有 SQL 都带 WHERE tenant_id = ?。如果联合索引不以 tenant_id 开头,MySQL 无法利用该索引跳过无关租户数据,等价于全表扫描——哪怕你只查自己租户的 10 条记录。
错误示例:INDEX (created_at, status) —— 即使查询写了 tenant_id 和 created_at,这个索引也几乎不会被选中。
正确做法:所有联合索引都把 tenant_id 放最左侧。例如:INDEX (tenant_id, created_at)、INDEX (tenant_id, status, updated_at)。
高频查询字段顺序决定索引后缀排列
联合索引的后续字段顺序,取决于实际 WHERE / ORDER BY 中的组合模式,不是按“重要性”排,而是按「最左前缀匹配」规则对齐查询条件。
比如常见查询是:SELECT * FROM orders WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC,那么索引应为:INDEX (tenant_id, status, created_at)。
注意以下几点:
- 如果还有查询用到
status和pay_status两个字段,但没带created_at,那INDEX (tenant_id, status, pay_status)就比(tenant_id, status, created_at)更通用; -
ORDER BY字段必须紧接在 WHERE 等值条件之后(且不能有范围条件隔开),否则无法走索引排序; - 避免把高基数字段(如
uuid)放在中间,会截断索引生效范围。
区分读写比例,谨慎添加覆盖索引
覆盖索引(即 SELECT 的所有字段都在索引里)能避免回表,但代价是索引体积变大、写入变慢。多租户系统通常写入压力不小,尤其当 tenant_id + 其他字段组合导致索引行数爆炸时,得权衡。
典型误用:INDEX (tenant_id, user_id, amount, remark, created_at) —— 把 remark(TEXT 类型)加进索引,不仅无效(MySQL 不支持 TEXT 前缀索引以外的直接索引),还会让索引页严重膨胀。
建议做法:
- 只把
SELECT中高频出现的、宽度小的字段加入索引末尾,如user_id、status、amount; - 对
VARCHAR(255)以上字段,宁可回表,也不要无脑覆盖; - 用
EXPLAIN FORMAT=TRADITIONAL确认Extra列是否含Using index,而不是凭感觉加字段。
分区表和联合索引不是替代关系
有人想用 MySQL 表分区(如按 tenant_id HASH 分区)来代替联合索引,这是危险的。分区裁剪依赖严格等于条件,且仅对分区键生效;而联合索引支持范围、IN、ORDER BY 等多种访问模式。
更现实的情况是:分区 + 联合索引共存。例如按 tenant_id 分区后,每个分区内部仍需 INDEX (tenant_id, created_at) —— 因为分区键在二级索引中仍是逻辑存在,不加 tenant_id 前缀,索引依然失效。
特别注意:ALTER TABLE ... PARTITION BY HASH(tenant_id) 后,若忘记重建联合索引,原有索引可能因元数据变更而性能骤降,务必验证 EXPLAIN 结果。
真正难的不是怎么建索引,而是判断哪些查询路径必须走索引、哪些可以接受慢一点——租户数据倾斜、冷热分离、归档策略都会影响索引价值。上线前拿真实租户的慢查日志反向推索引,比凭空设计靠谱得多。











