不能只给 tenant_id 单独建索引,因其选择性差、无法覆盖组合查询条件,易导致全表扫描或回表开销大;联合索引必须以 tenant_id 为最左字段,并按高频等值→低选择性等值→范围查询顺序排列,且需为不同查询模式分别建立针对性联合索引。

为什么不能只给 tenant_id 单独建索引
单列 tenant_id 索引在多租户查询中看似合理,但实际效果常不理想。因为绝大多数业务查询不是“查所有某租户的数据”,而是“查某租户下满足状态+时间范围的订单”这类组合条件。MySQL 在 WHERE tenant_id = ? AND status = ? AND created_at > ? 场景下,若只有 tenant_id 索引,优化器大概率会走全表扫描或仅用该索引做初步过滤,再回表筛选其余字段,I/O 开销大。更关键的是,tenant_id 值分布通常高度倾斜(比如大客户数据量占 80%),导致索引选择性差,统计信息不准时还会引发执行计划退化。
联合索引字段顺序必须是 tenant_id 在最左
这是硬性前提:所有带 tenant_id 的查询都依赖它作为隔离边界。MySQL 的最左前缀匹配规则决定了,如果把 status 放最左,那 WHERE tenant_id = 123 就完全用不上这个索引。实操中按以下原则排后续字段:
- 高频等值查询字段紧随其后,例如
status、type、is_deleted - 范围查询字段(如
created_at、updated_at)放最后,因为一旦出现范围条件,后续字段无法被索引使用 - 避免把低选择性字段(如只有 2–3 个取值的
is_active)放在等值段中间,会浪费索引区分度
典型例子:INDEX (tenant_id, status, is_deleted, created_at) 能覆盖 WHERE tenant_id = ? AND status = ? AND is_deleted = 0,也能用于 ... AND created_at > ?(但此时 created_at 仅参与范围扫描,不支持排序或跳过)。
需要为不同查询模式建多个联合索引,而不是试图“一索引通吃”
一个表常有多种租户查询路径:列表页按状态+时间分页、后台导出按类型+更新时间、统计任务按租户+日期聚合。指望单个联合索引兼顾所有场景,结果往往是每个都不高效。正确做法是识别主干查询 SQL,针对性建索引:
- 列表接口:
WHERE tenant_id = ? AND status IN (?, ?) ORDER BY updated_at DESC LIMIT 20→ 建INDEX (tenant_id, status, updated_at) - 定时任务扫描:
WHERE tenant_id = ? AND updated_at → 建 <code>INDEX (tenant_id, is_synced, updated_at) - 聚合统计:
GROUP BY tenant_id, DATE(created_at)→ 考虑INDEX (tenant_id, created_at)(利用索引有序性加速分组)
注意:不要为每个 WHERE 子句都建索引,先用 EXPLAIN 确认未命中索引的真实瓶颈;索引总数建议控制在 5–7 个以内,过多会影响写入性能和优化器决策。
分区表 + 联合索引不是银弹,多数场景反而增加运维负担
有人会想到按 tenant_id 分区(LIST 或 HASH),再配合联合索引。但 MySQL 的分区表对跨分区查询(如 tenant_id IN (1,2,3))支持弱,且分区键必须是主键/唯一键的一部分,这常与业务主键冲突。更现实的问题是:分区后 ALTER TABLE 变得极慢,备份恢复复杂,监控粒度变粗。除非单租户数据量稳定超千万行且查询严格限定单租户,否则优先用联合索引 + 合理分库分表策略。真正关键的是确保所有 INSERT/UPDATE/DELETE 语句都显式带上 tenant_id 条件——漏掉这个,再好的索引也救不了全表锁或慢查询。


















