tenant_id必须是联合索引第一列,因它是高频等值过滤条件,B+Tree索引需从最左列开始匹配才能跳过无关租户数据页,否则将触发全表扫描。

多租户系统中,tenant_id 必须作为几乎所有查询的强制过滤条件;不把它放在复合索引最左侧,等于没建索引。
为什么 tenant_id 必须是联合索引的第一列
租户数据物理隔离靠的是 WHERE tenant_id = ?,这是高频、高选择性、且几乎从不单独缺失的等值条件。InnoDB 的 B+Tree 索引按顺序存储,只有从最左列开始匹配才能跳过无关租户的数据页。一旦跳过 tenant_id(比如只查 status 或 created_at),MySQL 就得扫描全部租户的数据——在千万级数据下,type: ALL 和 rows: 2000000+ 是常态。
常见错误现象:
-
EXPLAIN显示key为NULL,即使表上存在(status, created_at)索引 - 同一 SQL 在单租户库飞快,在多租户库慢 10 倍以上
- 监控发现大量
Using where; Using filesort,但字段明明有索引
tenant_id + 查询模式决定后续列顺序
加了 tenant_id 只是起点,后续列顺序必须贴合真实 WHERE 条件组合和排序需求,不是按字段重要性拍脑袋排。
典型场景与建议:
- 订单列表页:常查
WHERE tenant_id = ? AND status IN (?, ?) ORDER BY created_at DESC→ 建INDEX idx_tenant_status_created (tenant_id, status, created_at)。注意:status是等值或IN,放中间;created_at是范围 + 排序,放最后 - 用户搜索:常查
WHERE tenant_id = ? AND (name LIKE '张%' OR email LIKE 'zhang%')→ 单独建tenant_id前缀索引不够,需搭配前缀索引,如INDEX idx_tenant_name_prefix (tenant_id, name(10)) - 统计报表:常查
GROUP BY tenant_id, date(created_at)→ 避免函数,改用created_at >= ? AND created_at ,索引设为 <code>(tenant_id, created_at),让range扫描生效
警惕 tenant_id 索引的三个隐形失效点
tenant_id 在索引最左 ≠ 一定被用上。以下情况会让它“形同虚设”:
-
tenant_id字段类型不一致:应用传字符串'123',而列定义是INT→ 触发隐式类型转换,索引失效 - 使用了负向条件:如
WHERE tenant_id != 123或tenant_id NOT IN (123, 456)→ MySQL 放弃使用该索引,改走全表扫描 - 联合索引中混入范围条件后还指望右列生效:例如索引是
(tenant_id, status, created_at),但查询写成WHERE tenant_id = 123 AND status > 0 AND created_at > '2025-01-01'→created_at列无法利用索引(范围之后的列失效)
如何验证 tenant_id 索引是否真起效
别只看有没有建,要看执行时是否真正穿透到数据页:
- 用
EXPLAIN FORMAT=TRADITIONAL查key是否非空、key_len是否符合预期(比如tenant_id INT占 4 字节,tenant_id + status TINYINT应为 5) - 检查
Extra:出现Using index condition表示启用了 ICP(索引下推),说明tenant_id后的过滤也下推到了引擎层;若只有Using where,说明部分条件还在 Server 层过滤 - 对比
rows:相同查询在单租户库 vs 多租户库的预估扫描行数,差距不应超过一个数量级;否则说明索引未对齐实际查询模式
最易被忽略的是租户数据分布倾斜——某个大租户占了 80% 数据量,而索引本身健康,但查询仍慢。这时需要结合分区(RANGE COLUMNS(tenant_id))或冷热分离,单靠调索引解决不了根本问题。


















