在MySQL项目中,索引经常出现一种很有意思的现象:数据库刚开始只有几张表时,开发人员嫌查询慢,于是不断添加索引;系统运行几年以后,一张核心业务表可能挂着十几个甚至几十个索引。查询似乎确实变快了,但INSERT、UPDATE、DELETE越来越重,磁盘空间不断增加,Buffer Pool里也需要缓存更多索引页。到了这个阶段,真正需要解决的问题已经不是“还能不能再加一个索引”,而是“哪些索引其实已经没有存在的必要”。
MySQL官方明确指出,不必要的索引会浪费存储空间,也会增加优化器选择索引的成本,同时INSERT、UPDATE和DELETE都需要维护相关索引。因此,索引优化的目标从来不是数量越多越好,而是找到能够覆盖主要查询模式的最小有效索引集合。
首先要纠正一个常见说法:MySQL并没有规定一个表“最多5个索引”。同样,也不存在“索引通常占表数据10%—30%”这样的通用比例。索引大小取决于索引列的数据类型、长度、基数、复合索引结构、主键长度以及数据量。尤其是InnoDB,二级索引记录中还包含主键值,因此主键过长会放大所有二级索引的存储和I/O成本。
判断一个索引是否值得保留,第一步不是看索引数量,而是看真实SQL。例如一张users表存在(user_id,status)和(user_id,status,created_at)两个索引,它们并不意味着后者一定是前者的简单重复。前一个索引可能服务于按照user_id和status过滤的查询,后一个则可能进一步支持时间范围、排序或者覆盖查询。真正需要分析的是查询模式和执行计划,而不是单纯比较索引名称。
反过来,如果存在INDEX(user_id),而所有主要查询实际上都是按照user_id,status或者user_id,created_at访问,那么单列索引就可能已经失去独立价值。但也不能仅凭“有一个更长的复合索引”就立即删除它,因为不同查询可能对列顺序、范围条件和排序有不同要求。
复合索引尤其需要注意左侧列顺序。假设SaaS系统经常执行:
SELECT id, title
FROM orders
WHERE tenant_id = ?
AND status = ?
ORDER BY created_at DESC
LIMIT 50;
那么类似(tenant_id,status,created_at)的索引就可能比分别建立tenant_id、status和created_at三个单列索引更符合实际访问模式。MySQL官方也建议,对于存在多种查询组合的InnoDB表,与其给每个字段分别建立大量索引,不如根据实际查询模式设计少量合适的复合索引。
但是,“选择性最高的列永远放在最左边”同样不是可靠规则。复合索引的顺序应该根据WHERE条件、等值匹配、范围条件、JOIN、ORDER BY、GROUP BY以及实际执行计划共同决定。例如tenant_id,status,created_at在多租户SaaS中可能非常合理,即使tenant_id本身的基数并不一定最高,因为它首先承担的是租户范围限定,后面的status和created_at再进一步缩小访问范围。
低基数字段也不能简单判定为“不能建立索引”。例如status只有active和paused两个值,单独建立status索引在某些数据分布和查询场景下确实可能价值有限,但如果查询只需要访问很小比例的数据,或者status与其他字段组成了有效的复合索引,它仍然可能有意义。MySQL官方说明,索引值对应的平均行数越多,该索引通常越不利于优化器选择;最终仍然需要结合实际数据分布判断。
SHOW INDEX可以查看索引结构和统计信息,例如:
SHOW INDEX FROM orders;
其中Cardinality表示索引中不同值数量的估计值,但它不是精确计数,更不能规定“低于1000就删除”。例如一个拥有1000万行的表,cardinality为800可能意味着选择性很低;而一张只有5000行的表,cardinality为800可能完全不同。MySQL官方也明确说明,InnoDB的cardinality属于统计估算值,并不是精确数字。
因此,看到key=NULL、type=ALL或者Extra中的Using filesort,也不能直接宣布“索引坏了”。type=ALL表示当前执行计划进行了全表扫描,但小表全表扫描可能比走索引更合理;Using filesort表示需要额外排序,也不意味着一定存在严重性能问题;Using temporary同样需要结合具体SQL、数据量和执行耗时判断。真正有价值的是使用EXPLAIN和EXPLAIN ANALYZE观察查询实际访问了多少行、执行时间以及具体访问路径。
另一个容易被误解的是覆盖索引。假设:
SELECT user_id, status
FROM users
WHERE user_id = ?
AND status = 'active';
如果存在合适的复合索引,MySQL可能直接从索引中取得所需字段,而不需要再次读取聚簇索引中的完整行。但覆盖索引并不是“字段越多越好”。索引越宽,占用的磁盘和内存越多,写入时维护成本也越高。对于高写入SaaS系统,一个为了覆盖极少数查询而建立的超宽索引,可能得不偿失。
真正需要警惕的是“索引堆积”。例如开发过程中先建立tenant_id,后来建立tenant_id,status,再建立tenant_id,status,created_at,最后又因为某个报表查询增加tenant_id,status,created_at,id。这些索引之间可能存在明显重叠,但究竟能否删除其中一个,必须逐条对应真实SQL验证。
生产环境尤其不要直接执行DROP INDEX然后祈祷业务没事。MySQL支持Invisible Index,可以先把普通二级索引设置为不可见,让优化器暂时不使用它,再观察关键查询是否出现执行计划变化或性能退化。这比直接删除索引更适合风险控制。确认没有实际依赖后,再决定是否正式删除。
例如:
ALTER TABLE orders
ALTER INDEX idx_orders_status INVISIBLE;
观察一段时间后,如果确认没有问题,再执行:
DROP INDEX idx_orders_status
ON orders;
索引维护还需要区分ANALYZE TABLE和OPTIMIZE TABLE。ANALYZE TABLE主要用于更新优化器所使用的统计信息,包括索引基数等;MySQL会利用这些统计信息帮助选择执行计划。对于数据分布发生重大变化的表,重新分析统计信息可能非常重要。
而OPTIMIZE TABLE不是“定期整理所有索引”的万能命令。对于InnoDB,它主要通过重建表来重新组织物理存储、更新索引统计信息并在适当情况下回收空间。是否需要执行,应根据大量删除、更新造成的空间和存储问题判断,而不是把它当成日常索引保养命令。
实际SaaS项目中,删除索引之前最好建立一条完整的证据链:先统计业务SQL,再检查EXPLAIN和EXPLAIN ANALYZE,查看慢查询日志和APM,确认索引是否服务于核心查询,同时观察INSERT、UPDATE、DELETE的负载。如果条件允许,可以先使用Invisible Index测试。最终决定删除以后,再持续观察数据库CPU、I/O、锁等待、查询延迟和执行计划。
还有一个容易踩坑的地方,就是不要因为某个字段“经常参与JOIN”就机械地建立索引,也不要因为表“只有几百行”就制定永久规则禁止索引。数据库优化没有一个适用于所有项目的行数阈值。小表可能根本不需要索引,大表也可能因为查询模式特殊而不需要某个看起来很合理的索引。
真正成熟的索引设计,其实是一种持续的工程管理。每建立一个索引,都应该能够回答三个问题:它服务哪些重要SQL?它解决了什么访问路径问题?它增加了多少写入和存储成本?如果一个索引长期找不到明确的业务查询,却持续占用空间并参与维护,那么它就应该进入删除评估名单。
MySQL索引优化的核心,从来不是“索引越多查询越快”,而是让有限的索引精准覆盖真正重要的访问路径。好的数据库设计不是把所有可能的查询都提前做成索引,而是在真实业务、执行计划、数据分布和写入成本之间找到一个可以长期维护的平衡。对于不断增长的SaaS系统来说,定期清理重复、重叠和已经失去业务价值的索引,往往和添加新索引同样重要。
MySQL 索引不是越多越好,什么时候应该减少无用索引
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP