联合索引的设计常被简化成一句“遵守最左前缀原则”,但真正的生产问题通常更复杂:等值条件和范围条件谁在前,排序能否利用索引,索引列是不是越多越好,以及执行计划显示用了索引为什么仍然很慢。
从查询开始,而不是从字段开始
假设订单表的核心查询如下:
1 | SELECT id, user_id, status, created_at, amount |
一个常见索引是:
1 | CREATE INDEX idx_orders_user_status_created |
user_id 和 status 是等值条件,created_at 同时承担范围过滤和排序。这个顺序使存储引擎先定位某个用户和状态下的连续索引区间,再按时间读取所需记录。
列顺序不能只看区分度
“区分度最高的列放最前面”只是经验,不是定律。索引要服务具体查询。如果系统总是先按租户隔离数据,tenant_id 即使区分度不高,也通常应处于索引前部,否则无法稳定限制扫描范围,也不利于权限边界表达。
设计时依次考虑:查询是否存在固定前缀、哪些列使用等值匹配、何处出现范围条件、是否需要支持排序与分组、索引能否被多条高频查询复用。
范围条件之后发生了什么
对于 (user_id, status, created_at),前两列等值匹配后,MySQL 可以利用第三列定位时间范围。但如果索引后面还有 amount,查询条件 amount > 100 通常不能继续缩小用于定位的索引区间。
这不代表后续列完全无用。启用索引条件下推时,存储引擎可能在索引层过滤部分记录,减少回表。因此应区分“用于索引定位”和“用于索引层过滤”,不要把 key 非空简单理解成整条条件都被高效利用。
覆盖索引的收益与成本
如果查询只返回 id、status 和 created_at,而这些值都能从二级索引获得,就可能避免回表。覆盖索引对高频列表查询很有价值,但不应把所有返回列都塞进索引。
宽索引会增加磁盘占用、降低缓存命中率,并放大插入和更新成本。长度较大的文本列、频繁变化的列尤其不适合为了覆盖查询而随意加入。生产设计需要在读收益和写成本之间取舍。
用 EXPLAIN ANALYZE 验证
MySQL 8 可以直接观察计划估算与真实执行数据:
1 | EXPLAIN ANALYZE |
重点关注实际读取行数、过滤后行数、循环次数、是否出现额外排序,以及估算与实际值是否相差过大。若估算严重失真,应检查统计信息和数据分布,而不是立刻强制指定索引。
Using filesort 也不必一概视为故障。对很小的结果集,排序成本可能低于维护一个额外索引。优化目标是降低真实查询成本,而不是消灭执行计划中的某个词。
分页越往后为什么越慢
即使索引正确,下面的深分页仍需要跳过大量记录:
1 | SELECT id, created_at |
更稳定的方式是游标分页:
1 | SELECT id, created_at |
对应索引可设计为 (user_id, created_at DESC, id DESC)。游标分页避免扫描并丢弃前十万条记录,也能在数据持续写入时提供更稳定的翻页结果。
索引治理不能忽略写入
每个二级索引都会参与写入。重复索引、前缀被完全覆盖的索引和长期未使用索引,会持续消耗存储与写性能。删除前应结合慢查询、索引使用统计、业务周期和回滚方案验证,不能仅凭短时间观察下结论。
总结
联合索引没有脱离查询的“标准答案”。从高频 SQL 出发,明确等值、范围、排序和返回列,再用 EXPLAIN ANALYZE 验证真实扫描量。最左前缀是理解 B+Tree 使用方式的起点,不是索引设计的终点。