在SaaS系统、订单系统和后台管理系统中,一个查询往往同时带有租户、状态、用户、时间和排序条件。这个时候,单列索引往往不够用,联合索引就成为数据库性能优化的重要工具。
但联合索引最容易被误解的地方,恰恰是“索引顺序”。很多教程喜欢总结成“高选择性字段放前面”或者“查询频率最高的字段放最前面”,这些说法可以作为思考方向,却不能当成固定规则。真正合理的索引设计,需要从实际SQL、过滤条件、范围查询、ORDER BY、数据分布以及写入成本一起判断。
一、联合索引到底是怎样工作的
假设建立一个联合索引:
CREATE INDEX idx_user_status_time
ON orders (user_id, status, create_time);
对于InnoDB常见的B+Tree索引结构,可以把它理解成按照 (user_id, status, create_time) 这个组合键进行有序排列。MySQL官方文档明确说明,联合索引可以用于索引中最左侧的一列、前两列或者全部列。例如 (a,b,c) 可以形成 (a)、(a,b) 和 (a,b,c) 这些左侧前缀的索引查找能力。
因此下面三个查询具有不同程度的索引利用能力:
WHERE user_id = 1001
WHERE user_id = 1001
AND status = 'paid'
WHERE user_id = 1001
AND status = 'paid'
AND create_time >= '2026-01-01'
而:
WHERE status = 'paid'
不能把 (user_id,status,create_time) 当成一个以 status 开头的索引来进行普通的左侧前缀查找。原因并不是“status字段完全不能参与过滤”,而是索引树首先按照 user_id 排序,单独寻找所有 status='paid' 的记录无法形成一个简单的连续索引区间。
这里还有一个非常重要的细节:SQL中的条件书写顺序与索引定义顺序不是一回事。
例如:
WHERE user_id = 1001
AND status = 'paid'
和:
WHERE status = 'paid'
AND user_id = 1001
逻辑上相同,优化器不会因为你把 status 写在前面,就要求索引必须是 (status,user_id)。真正决定索引能否形成有效访问路径的是条件与索引列之间的关系,而不是WHERE子句文字排列顺序。
二、联合索引顺序不能简单套用“选择性越高越靠前”
“高选择性字段放最左边”是非常常见的一句话,但如果把它当成绝对规则,很容易设计出并不适合实际业务的索引。
假设:
SELECT *
FROM orders
WHERE tenant_id = 100
AND status = 'paid'
AND create_time >= '2026-01-01';
一种很常见的设计是:
CREATE INDEX idx_orders
ON orders (tenant_id, status, create_time);
如果SaaS系统的大多数查询都是围绕租户进行,而且查询经常同时按照状态和时间过滤,那么这个顺序非常自然。
如果把 create_time 放在最前面:
(create_time, tenant_id, status)
当然不是说索引“完全失效”。时间范围仍然可以形成索引范围扫描,但后面的tenant_id和status能否继续缩小索引扫描范围、能否帮助排序等,需要根据具体查询和优化器执行计划判断。多列索引的范围访问本身就是一个成本优化问题,不能简单理解成“后面的列全部不能用”。
所以更实用的思路是:先找出业务中最稳定、最常见的查询形态,再决定索引顺序。
三、等值条件和范围条件通常是设计联合索引的关键
在很多典型查询中,可以优先考虑把稳定的等值过滤条件放在前面,再考虑范围条件。
例如:
SELECT id, user_id, status, create_time
FROM orders
WHERE tenant_id = 100
AND status = 'paid'
AND create_time >= '2026-01-01'
AND create_time < '2026-02-01';
可以考虑:
CREATE INDEX idx_orders
ON orders (tenant_id, status, create_time);
这里tenant_id和status都是等值条件,create_time是范围条件。
这种结构通常能够先把索引搜索范围缩小到某个租户、某种状态,再在对应的索引范围内进行时间扫描。
但是,“等值字段一定要按照选择性从高到低排列”同样不是绝对规则。
例如:
(tenant_id, status, create_time)
和:
(status, tenant_id, create_time)
在某个特定SQL中可能产生相似的访问路径,而在其他SQL中却完全不同。
如果系统还有大量查询:
WHERE tenant_id = ?
那么以tenant_id开头的索引可能可以服务更多查询。
这就是为什么联合索引设计不能脱离SQL列表单独讨论。
四、一个联合索引可以服务多个查询,但不能什么都兼顾
例如:
CREATE INDEX idx_orders
ON orders (tenant_id, status, create_time);
它可能同时帮助:
WHERE tenant_id = ?
WHERE tenant_id = ?
AND status = ?
WHERE tenant_id = ?
AND status = ?
AND create_time >= ?
但如果业务中还有大量:
WHERE status = ?
那么这套索引并不能自动替代一个以status开头的索引。
因此设计索引时不能只看某一条SQL,而应该把同一张表的重要查询模式放在一起分析。MySQL官方也建议建立一组能够服务多个相关查询的索引,而不是为每一个查询都建立一个新索引。
五、范围条件之后的列要特别小心
这是联合索引设计中非常容易踩坑的地方。
例如:
SELECT *
FROM orders
WHERE user_id = 1001
AND create_time >= '2026-01-01'
ORDER BY update_time;
有人可能直接设计:
CREATE INDEX idx_orders
ON orders (user_id, create_time, update_time);
然后认为这个索引同时完成了user_id过滤、create_time范围过滤以及update_time排序。
这种理解并不准确。
因为索引的排序顺序是:
user_id → create_time → update_time
当create_time已经进入一个范围后,update_time并不是在整个扫描范围内保持一个可以直接满足ORDER BY update_time的全局连续顺序。因此不能简单认为“把ORDER BY字段放在范围字段后面,就自动消除排序”。
如果排序非常重要,就必须把实际SQL、WHERE条件、ORDER BY、LIMIT以及数据分布一起放进EXPLAIN或EXPLAIN ANALYZE验证。
MySQL的优化器可以采用索引排序、范围扫描、额外排序等不同方案,最终执行计划才是判断依据。
六、等值条件下,索引顺序往往比想象中更灵活
例如:
WHERE user_id = 1001
AND status = 'paid'
下面两个索引在这个特定查询中都可能有效:
(user_id, status)
以及:
(status, user_id)
因为两个字段都是等值条件。
但如果还有:
WHERE user_id = 1001
那么第一种索引明显具有额外价值,因为user_id是它的最左前缀。
所以索引顺序真正重要的地方,不是“数据库要求WHERE里的字段必须按照索引顺序写”,而是要考虑一组查询能否共享同一个索引。
七、OR并不意味着索引一定失效
原稿中这一说法需要纠正。
例如:
SELECT *
FROM users
WHERE id = 1
OR name = 'Alice';
不能简单得出“OR导致索引失效”。
MySQL存在Index Merge等优化方式,在特定条件下可以分别利用不同索引,然后合并结果。官方文档也明确提到,对于多个单列索引,优化器可能选择Index Merge,或者选择能够排除更多行的索引。
当然,OR查询是否真的高效仍然要看数据量、选择性、索引结构以及实际执行计划。
因此看到OR以后,正确做法不是立即删掉索引,而是:
EXPLAIN
SELECT ...
必要时进一步使用:
EXPLAIN ANALYZE
SELECT ...
看真实执行过程。
八、函数作用在索引列上确实需要特别注意
例如:
WHERE YEAR(create_time) = 2023
传统的普通索引:
INDEX(create_time)
通常无法像下面这种范围条件一样直接利用create_time的连续范围:
WHERE create_time >= '2023-01-01'
AND create_time < '2024-01-01'
后者通常更适合普通B+Tree索引的范围访问。MySQL官方文档也将范围条件作为B-tree索引的重要访问方式。
如果业务确实需要对表达式进行索引,MySQL新版本也支持functional key parts,在适合的场景下可以针对表达式建立索引,而不是只能依靠普通列索引。
九、字符串和数字之间的类型转换不能简单说成“索引失效”
例如:
WHERE id = '123'
如果id是INT,不能直接得出“索引一定失效”。
MySQL会进行类型转换,最终是否采用索引、采用什么访问方式,要看具体数据类型、表达式以及执行计划。
更重要的工程原则是:应用层应该尽量保持参数类型与数据库字段类型一致。
例如数据库定义:
id BIGINT
应用程序就应该把它作为整数ID处理,而不是长期依赖隐式类型转换。
这样既减少歧义,也有利于保持稳定的查询行为。
十、覆盖索引不是“联合索引的升级版”
假设:
CREATE INDEX idx_user
ON users (user_id, name, email);
查询:
SELECT name, email
FROM users
WHERE user_id = 1001;
如果执行计划能够实现Index Only Scan,也就是所需字段已经可以从索引中取得,那么就可能减少访问聚簇记录的需要。
这就是覆盖索引的价值。
但是覆盖索引也有成本。
如果为了覆盖查询而把十几个字段全部塞进索引:
(user_id, name, email, phone, address, ...)
索引会明显变宽,磁盘空间、Buffer Pool占用以及INSERT、UPDATE、DELETE维护成本都会增加。
所以覆盖索引应该针对高频、关键查询设计,而不是“只要能覆盖就把所有字段都加进去”。
MySQL官方同样强调,索引并不是越多越好,因为每个索引都会产生存储和写入维护成本。
十一、Index Condition Pushdown不要和“索引能使用”混为一谈
原稿中把:
(user_id, status)
配合:
WHERE user_id = 1001
AND status = 'paid'
直接称为Index Condition Pushdown示例,并不准确。
这个查询本身已经可以直接利用联合索引进行有效查找。ICP是MySQL在特定访问路径下,将部分索引条件进一步下推到存储引擎层过滤,以减少不必要的记录读取,并不是“只要联合索引有两个字段就叫ICP”。
因此,在实际排查时不要看到一个多列索引就把所有优化都归类为“索引下推”。应该通过执行计划判断优化器到底采用了什么访问方式。
十二、ANALYZE TABLE是更新统计信息,不是“整理索引”
联合索引设计完成以后,可以根据实际情况执行:
ANALYZE TABLE orders;
它的主要作用是帮助优化器获得更准确的索引统计信息,从而改善执行计划选择。MySQL官方也建议在某些InnoDB索引创建后更新统计信息。
但不要把:
ANALYZE TABLE
理解成“定期整理索引”。
同样,也不要把:
OPTIMIZE TABLE
当成日常索引保养工具。两者解决的问题不同。
十三、真正判断联合索引是否设计正确,要看执行计划
一个索引名字看起来再漂亮,也不能证明设计正确。
实际应该观察:
EXPLAIN
SELECT ...
以及在适合的生产或测试条件下:
EXPLAIN ANALYZE
SELECT ...
重点关注的并不是某一个字段变成绿色就算成功,而是整个执行过程,包括访问类型、实际扫描行数、估算行数、使用的索引、过滤情况、排序、临时表以及JOIN过程。
例如:
key
possible_keys
key_len
rows
filtered
Extra
都需要结合SQL和数据规模解释。
Using filesort也不等于“数据库性能一定很差”,key=NULL也不等于“索引一定失效”。优化器有时判断全表扫描比使用索引更便宜,尤其是表很小或者查询需要读取大量记录时。
因此,索引优化的目标不是让EXPLAIN看起来“全部使用索引”,而是让实际查询成本下降。
十四、SaaS系统尤其要考虑tenant_id
对于多租户SaaS系统,很多业务查询天然带有:
WHERE tenant_id = ?
这种情况下,tenant_id经常成为联合索引设计的重要组成部分,例如:
CREATE INDEX idx_orders_tenant_status_time
ON orders (tenant_id, status, create_time);
但也不能机械地给所有索引都加tenant_id。
如果某些后台任务本身按照订单ID、全局唯一编号或者时间范围处理全部租户数据,那么强行把tenant_id放进所有索引反而可能增加索引数量和宽度。
更需要注意的是,tenant_id进入索引只解决性能和访问路径问题,并不能自动解决租户数据隔离。真正的SaaS隔离还必须覆盖ORM、Repository、后台任务、缓存、消息队列以及权限检查。
十五、联合索引设计最实用的方法不是背口诀,而是整理查询矩阵
如果一张orders表有下面几类核心SQL:
tenant_id
tenant_id + status
tenant_id + status + create_time范围
tenant_id + user_id
order_no
tenant_id + create_time + LIMIT
那么应该先把这些查询按照频率、延迟、数据量和业务重要程度整理出来,再寻找能够覆盖多个查询的索引组合。
例如:
INDEX (tenant_id, status, create_time)
可能服务前三种查询。
而:
UNIQUE INDEX (order_no)
则解决订单号精确查找。
如果还存在:
WHERE tenant_id = ?
AND user_id = ?
再判断现有索引是否已经能够有效支持,还是确实需要:
INDEX (tenant_id, user_id)
这比按照“每个WHERE字段建立一个索引”的方式更加可靠。
联合索引设计最终不是一道数学题,也没有一个适用于所有系统的“最高选择性字段优先”公式。真正决定索引顺序的是业务查询模式:哪些条件长期稳定存在,哪些是等值匹配,哪些是范围查询,是否需要排序或分页,哪些查询最重要,以及一个索引能否服务多条核心SQL。
对于SaaS数据库,最危险的不是少建一个索引,而是在没有分析真实SQL的情况下不断加索引。索引越多,写入和更新成本越高,优化器需要考虑的访问路径也越多。MySQL官方同样强调,应当建立一组能够有效服务相关查询的索引,而不是为每一种可能的查询组合无限增加索引。
真正成熟的联合索引优化流程应该是:先收集慢查询和核心SQL,再根据查询模式设计候选索引,然后用EXPLAIN和EXPLAIN ANALYZE验证,最后结合CPU、I/O、Buffer Pool、锁等待、扫描行数以及写入成本观察上线后的实际效果。索引不是装饰品,也不是越多越安全,它本质上是为真实访问路径服务的数据结构。
MySQL 联合索引怎么设计?多条件查询中的索引顺序非常重要
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP