滚动新闻 →
北美时间制度迎来重大变化 阿省卑诗省告别每年两次调钟 波兰宪法法院人事争议升级 多名官员宣布辞职 中国籍男子涉贩毒洗钱 被引渡至美判刑12年 Google Cloud API 配额达到上限怎么办,应用扩容前应该检查哪些限制 美国中期选举进入最后倒计时 生活成本与移民政策成为选民投票焦点 MySQL 联合索引怎么设计?从 SaaS 查询条件理解字段顺序 川普佛州集会将中期选举定调 直指共产主义威胁并强化总统安全防卫 波兰宪法法院陷入权力争夺 两名驻外大使辞职引发政治风暴 波兰总统:你们不是宪法法院法官! 上海万圣节变装活动骤减 网友感叹氛围冷清 三名中国男子被胶带蒙眼捆绑在泰缅边境皮卡后座获救 刚果埃博拉疫情致超5000人丧生 疫情扩散至多国 美加夏令时调整引发健康争议 专家呼吁改革制度 五中全会44名中委候补委员缺席 涉及名单曝光 安徽高校启动六年前论文审查引争议 美日印澳联合谴责经济胁迫行为 美国中期选举临近 选民聚焦物价与移民议题 波兰司法风暴持续 法官停职引发宪政对峙 美日韩签署北极合作备忘录 拟牵制中俄影响力 吐槽问界M9大灯掉皮 车主:已大彻大悟 四川康定勒多曼因峰登山事故致2人遇难 中石化前董事长傅成玉退休11年后遭调查 欧洲多国抗议持续升级 意大利50城大规模示威 移动硬盘频繁断开连接,USB线、电源和硬盘本身怎么排查 川普佛州竞选活动警告共产主义威胁 台北市长选举电视辩论会 蒋万安沈伯洋激烈交锋 美军或于选举后对伊朗采取军事行动 克林顿押注中国市场开放 美国却低估了中共维持一党统治的能力 波兰政变局势动荡 驻中大使请辞引发跟进潮 法国动用7万警力应对示威 抗议浪潮席卷欧洲 俄弹药基地遭乌军突袭 俄军反击致28万户断电 伊朗升级军事威胁 美伊冲突风险陡增 退休十余年傅成玉被查 曾被指曾庆红亲信 沈蒋火爆硬碰硬!辩论后差距拉大?美11月4日轰伊朗? AI虚拟人亮相短剧盛典 真人演员生存空间被挤压 五中全会胡春华桌上红本子 又有最新说法 法国部署7万警力 严防万圣节抗议活动 北海道JR列车深夜撞熊致出轨 乘客滞留车内过夜 台积电ADR重挫5.95% 法人预警台股下周或现回档 浙江钱塘江大潮惊险一幕:男子被涌潮瞬间吞没

MySQL 联合索引怎么设计?从 SaaS 查询条件理解字段顺序

发布时间: 2026-10-31 16:00:03    最后更新: 2026-10-31 17:36:58    阅读:11  约19 分钟阅读     

MySQL 联合索引怎么设计?从 SaaS 查询条件理解字段顺序
图片说明:示意图   图片来源:Public Domain(公有领域)
SaaS系统中的数据库查询通常比普通单体应用复杂得多。一条订单查询可能同时涉及tenant_id、用户、状态、时间范围和排序;随着租户数量和业务数据持续增长,原本几毫秒的SQL可能逐渐变成几十毫秒甚至更高。

这时候,联合索引的重要性就体现出来了。

但联合索引最容易出现的误区,是把它变成一套固定口诀,例如“tenant_id一定放第一”“选择性最高的字段一定放第一”“时间字段一定放最后”。这些原则在部分查询中有帮助,却不能脱离具体SQL使用。真正合理的索引顺序,是让索引结构尽可能匹配核心查询的访问路径,同时控制存储空间和写入维护成本。

一、为什么SaaS系统特别容易需要联合索引

多租户系统通常会出现这样的查询:

SELECT *
FROM orders
WHERE tenant_id = 't123'
AND status = 'paid'
AND create_time >= '2026-01-01';

如果这类查询非常频繁,可以考虑:

CREATE INDEX idx_orders_tenant_status_time
ON orders (tenant_id, status, create_time);

这里的关键并不是“tenant_id是SaaS字段,所以必须排第一”,而是这条SQL恰好经常按照tenant_id、status和create_time进行过滤。

如果另一类核心查询完全不同,例如后台任务主要按照create_time扫描所有租户的数据,那么索引就可能需要采用不同结构。

因此,tenant_id经常是SaaS联合索引的重要组成部分,但并不是一条适用于所有查询的铁律。

二、联合索引真正重要的是左侧前缀

假设建立:

CREATE INDEX idx_orders
ON orders (tenant_id, status, create_time);

可以把它理解为按照:

tenant_id → status → create_time

形成有序的B+Tree索引结构。

它可以很好地支持:

WHERE tenant_id = ?

也可以支持:

WHERE tenant_id = ?
AND status = ?

以及:

WHERE tenant_id = ?
AND status = ?
AND create_time >= ?

但如果查询只有:

WHERE status = ?

就不能把这个索引当成一个以status为最左列的普通索引来使用。

这就是所谓的最左前缀原则。

MySQL官方文档明确说明,多列索引可以用于索引最左侧的一列、最左侧的两列,或者更长的左侧前缀。(dev.mysql.com)

需要特别注意的是,SQL中WHERE条件的书写顺序并不决定索引顺序。

下面两条SQL逻辑上是一样的:

WHERE tenant_id = 't123'
AND status = 'paid'
WHERE status = 'paid'
AND tenant_id = 't123'

不需要因为第二条SQL把status写在前面,就把索引改成(status,tenant_id)。

三、tenant_id为什么经常放在SaaS索引前面

如果一个SaaS数据库由很多租户共享,而且绝大多数业务查询都限定tenant_id,那么:

(tenant_id, status, create_time)

通常具有非常自然的业务意义。

例如:

WHERE tenant_id = ?
AND status = ?
AND create_time >= ?

索引可以首先进入某个租户对应的索引区域,再进一步按照status和create_time缩小范围。

但不能因此认为:

tenant_id

永远应该是所有索引的第一列。

例如:

SELECT *
FROM audit_logs
WHERE create_time >= ?
ORDER BY create_time
LIMIT 1000;

如果这是一个跨租户的后台归档任务,那么:

(create_time)

可能比:

(tenant_id, create_time)

更加符合这个查询。

索引设计服务的是查询,不是数据库里的字段分类。

四、状态字段为什么经常和tenant_id组成联合索引

SaaS订单系统中非常常见:

WHERE tenant_id = ?
AND status = 'paid'

因此:

(tenant_id, status)

可能非常有价值。

但status通常属于低基数字段。例如订单状态可能只有pending、paid、cancelled等几个值。

这并不意味着status不能建立索引。

如果单独建立:

(status)

由于值重复程度高,单独使用时可能没有很好的过滤效果;但放在:

(tenant_id, status, create_time)

中,status是在已经限定租户之后继续缩小搜索范围,其实际价值可能完全不同。

所以不能通过:

COUNT(DISTINCT status) / COUNT(*)

然后规定“超过0.1才能建立索引”。

没有一个适用于所有MySQL表的统一选择性阈值。

索引是否有价值,最终仍然应该通过实际查询、数据分布和执行计划判断。

五、等值条件、范围条件和排序要一起考虑

例如:

SELECT id, amount
FROM orders
WHERE tenant_id = ?
AND status = ?
AND create_time >= ?
AND create_time < ?
ORDER BY create_time DESC
LIMIT 100;

一个很自然的候选索引是:

CREATE INDEX idx_orders
ON orders (tenant_id, status, create_time);

因为tenant_id和status属于等值过滤,create_time属于范围条件,同时查询还按照create_time排序。

这种结构有机会同时帮助过滤和排序。

但如果SQL变成:

WHERE tenant_id = ?
AND create_time >= ?
ORDER BY update_time DESC

再建立:

(tenant_id, create_time, update_time)

就不能简单宣称“这个索引既过滤create_time又完成update_time排序”。

因为create_time已经进入范围以后,后面的update_time并不自动保持整个扫描结果所需要的排序关系。

所以对于复杂分页查询,必须用EXPLAIN甚至EXPLAIN ANALYZE验证,而不能只看索引定义就下结论。

六、不要把覆盖索引理解成“给SELECT *加几个字段”

覆盖索引确实可以减少回表。

例如:

CREATE INDEX idx_user
ON users (tenant_id, status, create_time, user_id);

如果查询只需要索引中已经存在的字段:

SELECT tenant_id, status, create_time, user_id
FROM users
WHERE tenant_id = ?
AND status = ?;

在合适的执行计划下,可能直接从索引获得所需数据。

但如果查询:

SELECT *
FROM orders
WHERE tenant_id = ?
AND status = ?;

即使索引包含tenant_id、status、create_time、amount等几个字段,只要SELECT需要的其他列不在索引中,就不能把它称为覆盖索引。

更不能为了覆盖SELECT *而把整张表的字段全部塞进索引。

索引越宽,存储成本和写入维护成本越高。

七、不要给每个查询条件建立一个索引

假设orders表存在:

(tenant_id)
(status)
(create_time)
(user_id)
(order_no)

然后又不断增加:

(tenant_id,status)
(tenant_id,create_time)
(tenant_id,user_id)
(status,create_time)
(user_id,status)

最后数据库可能拥有十几个甚至几十个索引。

查询看起来很“有安全感”,INSERT和UPDATE却开始付账单。

InnoDB需要维护这些索引,索引还会占用Buffer Pool和磁盘空间。MySQL官方同样强调,应该根据查询需要设计索引,而不是无限增加索引。(dev.mysql.com)

因此,一个重要原则是:

一个联合索引如果已经能够有效服务多个核心查询,就没有必要为了某一条低频SQL立即增加新的索引。

八、不要把“3个索引以内”当成规则

原稿提出百万级表最好保持三个索引以内,这同样没有通用依据。

一张小型业务表可能只需要两个索引,一张复杂订单表可能需要更多索引。

真正需要考虑的是:

这个索引服务哪些SQL?

查询频率是多少?

扫描了多少行?

增加索引以后查询减少了多少成本?

INSERT、UPDATE、DELETE增加了多少成本?

索引占用了多少磁盘和内存?

有没有其他索引已经能够覆盖相同的访问路径?

因此,“索引数量”本身不是性能指标。

十个合理的索引可能比三个错误的索引更加健康。

九、两个索引字段相同但顺序不同,不一定是重复索引

例如:

INDEX idx1 (tenant_id, status)
INDEX idx2 (status, tenant_id)

不能简单称为“重复索引”。

因为:

(tenant_id, status)

和:

(status, tenant_id)

拥有不同的左侧前缀。

如果核心查询包括:

WHERE tenant_id = ?

第一个索引具有直接价值。

如果另一类核心查询是:

WHERE status = ?

第二个索引可能承担不同的访问路径。

当然,如果两者的实际SQL都同时对tenant_id和status使用等值条件,那么其中一个是否还有存在价值,就需要通过查询集合和执行计划判断。

十、OR并不代表索引一定失效

例如:

SELECT *
FROM orders
WHERE tenant_id = ?
OR status = 'paid';

不能简单认为OR查询一定不能使用索引。

MySQL优化器在特定条件下可以采用Index Merge等访问方式,也可能选择其他执行策略。

最终应该让EXPLAIN告诉你实际发生了什么,而不是看到OR就宣布索引死亡。

同样:

WHERE id = '123'

如果id是整数,也不能简单说“字符串参数导致索引失效”。MySQL存在类型转换机制,具体执行计划需要结合数据类型和表达式判断。

工程上更好的做法是让应用层参数类型与数据库字段保持一致,而不是依赖隐式转换。

十一、函数条件确实需要特别注意

例如:

WHERE YEAR(create_time) = 2026

如果create_time存在普通B+Tree索引,这种写法通常无法像直接范围查询一样充分利用create_time的连续范围。

更适合普通时间索引的写法通常是:

WHERE create_time >= '2026-01-01'
AND create_time < '2027-01-01'

如果业务确实需要针对表达式建立索引,MySQL也支持functional key parts,可以根据具体版本和场景考虑函数索引。(dev.mysql.com)

十二、SaaS中的“超级租户”是一个容易被忽视的问题

联合索引按照tenant_id设计以后,还要考虑租户之间的数据分布。

假设普通租户只有几万条订单,但某个大型租户拥有数亿条订单,那么:

tenant_id = A

和:

tenant_id = BIG_CUSTOMER

虽然使用完全相同的索引,却可能拥有完全不同的查询成本。

这就是典型的数据倾斜问题。

因此,SaaS系统不能只看平均租户规模,还应该监控最大的几个租户,也就是所谓的super-tenant。

当少数大型租户占据绝大多数数据以后,可能需要考虑数据归档、独立数据库、独立分片或者其他隔离策略。

索引可以解决访问路径问题,却不能解决无限增长的数据分布问题。

十三、不要把动态创建索引当成日常优化策略

原稿提出业务高峰期使用一套索引、低谷期再创建另一套索引,这不是通常意义上的动态索引策略。

索引是数据库结构的一部分,不应该为了每天的业务高峰和低谷频繁创建、删除。

如果确实需要测试新索引,可以在支持的MySQL版本中研究Invisible Index等机制,在不立即让优化器使用索引的情况下进行验证,再根据实际结果决定是否正式采用。

更重要的是提前评估DDL对生产系统的影响,并根据具体版本、存储引擎和表规模选择合适的变更方式。

十四、前缀索引不是“覆盖前100个字”

例如:

CREATE INDEX idx_notes
ON orders(notes(100));

它表示索引只存储notes字段前100个字符用于索引比较,并不意味着它可以“覆盖前100个字的查询结果”。

对于:

WHERE notes LIKE 'refund%'

前缀索引可能有帮助。

但:

WHERE notes LIKE '%refund%'

普通B+Tree前缀索引通常无法像前缀匹配一样直接定位。

如果业务需求实际上是全文搜索,就应该考虑FULLTEXT或专门的搜索系统,而不是不断增加notes字段的前缀长度。

十五、索引压缩不能用一个固定SQL解决

原稿中的:

WITH PADDING

也不能简单解释成“启用索引压缩,并节省30%空间”。

InnoDB不同表空间、页结构、压缩方式和具体配置之间存在差异。索引空间占用应该通过实际数据库统计和存储结构评估,而不是给出一个固定节省比例。

对于SaaS数据库,更实际的办法是监控表和索引大小,并定期检查哪些索引真正产生价值。

十六、SHOW INDEX不能直接告诉你“索引使用率”

很多开发者会执行:

SHOW INDEX FROM orders;

然后期待看到“这个索引使用了多少次”。

实际上,SHOW INDEX主要提供索引定义和统计信息,例如索引名称、列顺序、Cardinality等,并不是完整的运行时索引使用率统计工具。

要判断索引是否真正有价值,应该结合:

EXPLAIN
EXPLAIN ANALYZE

慢查询日志、Performance Schema、监控系统以及实际业务指标综合判断。

尤其需要区分:

“优化器选择过这个索引”

和:

“这个索引让整个业务系统变快了”。

这是两个不同的问题。

十七、SaaS联合索引最实用的设计流程

真正开始设计索引之前,可以先把核心SQL整理出来。

例如orders表可能存在:

1. tenant_id + status
2. tenant_id + status + create_time范围
3. tenant_id + user_id
4. order_no精确查询
5. create_time范围扫描

然后针对这些SQL分别记录:

查询频率、平均扫描行数、P95/P99延迟、返回数据量、排序方式、分页方式以及写入频率。

接下来设计候选索引,而不是看到一个字段就建立一个索引。

例如:

CREATE INDEX idx_orders_tenant_status_time
ON orders (tenant_id, status, create_time);

然后通过:

EXPLAIN
SELECT ...

观察优化器选择。

对于适合的测试场景,再使用:

EXPLAIN ANALYZE
SELECT ...

验证实际执行过程。

如果一个查询原来扫描数百万行,现在只扫描很小的范围,同时P95延迟下降,而INSERT和UPDATE成本没有出现不可接受的增长,那么这个索引才真正体现出价值。

十八、联合索引不是越复杂越好

一个常见错误是看到查询条件很多,就把所有字段全部塞进一个索引:

(tenant_id, status, user_id, create_time, update_time, amount, type, ...)

这种索引看起来非常“全面”,实际上可能变得又宽又重。

联合索引的目的不是把WHERE里的所有字段复制进去,而是构建高价值的访问路径。

一个好的索引通常能够服务一组重要查询,同时保持合理的存储和写入成本。

对于SaaS系统尤其如此,因为数据量越大、写入越频繁,错误索引的成本也会被放大。

MySQL联合索引设计真正应该遵循的原则,不是“tenant_id永远第一”,也不是“选择性最高永远第一”,而是从真实查询出发。先确定哪些SQL最重要,再分析哪些字段是稳定的等值条件、哪些属于范围条件、是否存在排序和分页需求,然后根据左侧前缀和实际数据分布设计候选索引。

最终再用EXPLAIN、EXPLAIN ANALYZE和生产监控验证。

对于SaaS数据库来说,联合索引解决的是“怎样更快找到需要的数据”,而不是“数据量大了以后怎样继续无限扩展”。当数据规模继续增长,还必须同时考虑归档、分区、读写分离、缓存、数据库拆分以及超级租户的数据倾斜。

真正成熟的索引设计,从来不是把字段一个个塞进CREATE INDEX,而是让数据库结构与业务访问模式形成长期稳定的配合。查询模式变了,索引也应该重新评估;数据分布变了,原来有效的索引也可能失去价值。数据库优化最怕的不是索引少,而是没有证据却不断增加索引。

喜欢这篇报道?

使用下面的功能,方便以后继续阅读和分享 MNewsTV

设为 Google 新闻首选来源 让 Google 新闻优先显示 MNewsTV 的最新报道 ›
★ 我的收藏 查看已经收藏的文章
关于文章收藏 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。
分享这篇报道

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP