☰
MySQL索引优化实战:从B+树原理到创建删除的完整指南
2026/10/10 17:08:13 网站建设 项目流程

1. 为什么索引值得认真对待

先说个我自己的感受。刚入行那两年,我把索引当成“数据库的加分项”,表建完、数据能查出来就完事了,索引随手建一两个。直到有一次线上订单表数据量飙到千万级,一条 where 条件带上create_time的查询要跑 4 秒多,接口超时告警一个接一个,我才不得不坐下来认认真真研究索引的底层逻辑。那次教训之后,我把索引的创建、删除、优化当成 MySQL 的必修课,每个生产环境变更都变得格外小心。

索引说白了就是 MySQL 为了加速数据检索而维护的一套额外数据结构。没有索引的时候,WHERE 条件筛选等于“全表扫描”,每一行都要过一遍;有了索引之后,MySQL 可以像查字典一样,先到索引结构里定位,再回表取数据。数据量小的时候这个差距不明显,一旦超过百万级,差距就是几十倍甚至上百倍。索引不只是所谓的“性能优化手段”,它直接决定了你的业务能不能跑得动。

这篇文章我不会只停留在“CREATE INDEX 怎么写”这个层面,而是把索引用起来之后的整个逻辑链条捋清楚:为什么普通索引能快、聚簇索引和二级索引有什么区别、创建索引时要避开哪些会让你追悔莫及的坑、删除索引时又有什么看不见的副作用。无论你是刚学会建表的初级开发者,还是正在维护一个慢查询频发的业务系统,这篇文章的实操内容和经验总结都应该能派上用场。

2. 索引的底层逻辑:只有理解了数据结构,才知道该怎么建

2.1 B+树为什么能支撑千万级数据的快速检索

要搞懂索引的创建和删除,先得知道索引底层长什么样。MySQL InnoDB 引擎的索引结构是 B+ 树。B+ 树是一种多路平衡查找树,它不同于常见的二叉树,每个节点可以存储多个键值,而且叶子节点之间通过链表相连。

用一个生活化的类比来说明:全表扫描就像在一本没有目录的书里找某个关键词,你必须一页页翻过去;而 B+ 树索引相当于这本书最后附了一个多层次的目录,先找到章,再定位到节,然后顺着页码直接翻到目标页。B+ 树的高度一般只有 3 到 4 层,也就是说即使表里有上千万条记录,定位一条数据也只需要几次磁盘 IO 就能完成,而全表扫描要读上千个数据页。

B+ 树相比其他数据结构有几个关键优势:第一,非叶子节点只存储索引键(不存储数据),这意味着一个数据页能装下的索引项非常多,树的高度得以降低,IO 次数随之减少;第二,叶子节点有序排列且有链表相连,这让范围查询(比如 BETWEEN、大于小于)变得极其高效,找到起点之后顺着叶子链表一路往后读就行;第三,所有数据都在叶子节点,查询性能非常稳定,不会像某些数据结构那样出现大幅度的性能抖动。哈希索引虽然能在等值查询上做到 O(1) 复杂度,但它对范围查询无能为力,这也是 InnoDB 默认使用 B+ 树而不是哈希表的核心原因。

2.2 聚簇索引、二级索引和回表:三个绕不开的概念

InnoDB 的表其实就是一棵 B+ 树,数据是“物理地”存储在聚簇索引的叶子节点上的。聚簇索引通常就是主键索引,表里每一行完整的数据行都挂在主键 B+ 树的叶子节点上。因为数据行只能有一份物理存储,所以一个表只能有一个聚簇索引。

二级索引(也就是我们平时手动创建的那些普通索引)则完全不同。它的叶子节点存储的是索引列的值加上主键值。这句话非常关键——二级索引的叶子节点并不包含完整数据行,只包含“索引字段”和“主键”这两个东西。当你通过二级索引查询数据时,MySQL 会先到二级索引的 B+ 树里找到符合条件的记录,得到主键值,然后再拿着主键值到聚簇索引里查完整数据行,这个步骤就叫回表。

举个具体例子。假设订单表orders上有主键id,还有一个普通索引idx_user_id在user_id列上。执行SELECT * FROM orders WHERE user_id = 123时,MySQL 会先去idx_user_id这棵 B+ 树里找到user_id=123的记录,拿到对应的主键 id,再回到主键索引树里读取整行数据。这个过程中的两次查找就是回表的由来。如果查询语句只需要user_id和id两个字段(也就是索引里本身已经有的列),MySQL 就不需要回表,这种情况叫覆盖索引,查询效率会高很多。

搞清楚这个逻辑,你才能理解创建索引时为什么通常建议把查询频率最高的列作为索引前缀,为什么不要动不动就SELECT *,也才能理解为什么“索引不是越多越好”——因为每一棵二级索引 B+ 树都要占用磁盘空间,而且每次 INSERT、UPDATE、DELETE 操作都要同步维护这些索引树,索引数量越多,写入成本就越高。

2.3 创建索引之前,先想清楚这个索引要服务什么样的查询

很多人在建索引的时候属于“想起来就建”,没有任何规划。我见过最极端的例子,一张表上建了十多个单列索引,结果慢查询一个没解决,写入性能反而下降得很明显。索引不是为了“有”而建的,它服务的对象是具体的 SQL 查询模式。

在动手创建之前,建议你先理清几个问题:你的 WHERE 条件常用哪些列?哪些列是等值过滤?哪些列是范围过滤?排序用的什么字段?表连接用的关联键是什么?有没有高频的 GROUP BY 或者 DISTINCT 操作?比如WHERE user_id = ? AND status = ?这两列的等值组合出现得非常多,建一个(user_id, status)的联合索引要比分别在两个列上建两个单列索引更高效;再比如WHERE user_id = ? ORDER BY create_time DESC,如果只对user_id建索引,排序必然会用到文件排序(filesort),效率远低于在一个(user_id, create_time)联合索引里直接按照索引顺序输出结果。

索引本质上是“以空间换时间”的典型取舍。你要为读性能换取额外的磁盘开销和写入开销,所以在创建索引之前,最好先用慢查询日志或者 performance_schema 把线上真实的高频 SQL 捞出来,看看它们到底缺什么索引,而不是凭感觉拍脑袋。这是我在实践中学到的第一条法则:不要为了建索引而建索引,要让索引去服务真实的查询模式。

3. 创建索引的完整实操指南

3.1 三种创建索引的方式和适用场景

MySQL 里创建索引的方式并不只有一种,你可以根据场景灵活选择。最常用的有三类:

第一种是在建表时直接指定索引。比如:

CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(64) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时就规划好索引的好处是结构一目了然,适合新表上线。但问题是如果你后来才发现索引设计不合理,要用 ALTER TABLE 去改,表大的时候会涉及 DDL 锁表问题,操作窗口比较紧张。

第二种方式是用CREATE INDEX语句在已有表上添加索引。这是日常维护中最常用的方式:

CREATE INDEX idx_user_status ON orders(user_id, status);

CREATE INDEX的优点是语法清晰,专门就是干“加索引”这件事的,而且支持一次创建一个索引,不会影响表上已有的其他结构。注意CREATE INDEX不能用来创建主键索引,主键索引只能通过ALTER TABLE ADD PRIMARY KEY或其他方式定义,普通索引、唯一索引、联合索引、前缀索引都能用CREATE INDEX创建。

第三种方式是用ALTER TABLE添加索引:

ALTER TABLE orders ADD KEY idx_user_status (user_id, status);

ALTER TABLE的适用范围最广,不但可以加索引,还可以改表结构、加字段、改字段类型等。如果你修改表结构的同时要加索引,一条 ALTER 语句就能合并搞定。从功能角度讲,CREATE INDEX和ALTER TABLE ADD INDEX是等价的,你可以按习惯选择。

3.2 索引类型选择:普通索引、唯一索引、联合索引、前缀索引到底怎么选

新手最容易犯的一个错误,就是把所有字段都建成普通索引,从不考虑区分度和约束。实际上 MySQL 索引类型分得比较细,每种类型背后的设计意图完全不同,选错了要么性能达不到预期,要么引入不必要的开销。

普通索引(KEY 或 INDEX)是最基础的索引,它的唯一任务就是加速查询。像订单表的status、user_id这类需要频繁过滤但允许重复的列,适合建普通索引。

唯一索引(UNIQUE KEY)则在普通索引的基础上增加了一层唯一性约束,这不仅是性能优化工具,更是数据完整性保障。比如订单号order_no,业务上本来就不允许重复,直接建一个唯一索引,既能让查询走索引,又能从数据库层面挡住重复插入。需要提醒的是,唯一索引的写操作开销比普通索引略高,因为每次插入都要额外检查是否冲突,所以在没有唯一性需求的列上,不要顺手建唯一索引。

联合索引(多列索引)是性能优化里最值得深挖的内容。它的核心支持“最左前缀原则”,也就是查询条件如果包含联合索引的最左列(或最左连续的多列),就能用上这个索引。比如有一个联合索引(user_id, status, create_time),那么WHERE user_id = ?、WHERE user_id = ? AND status = ?、WHERE user_id = ? AND status = ? AND create_time BETWEEN ? AND ?都能命中索引;但如果查询条件跳过了user_id,只在status或create_time上过滤,这个联合索引就帮不上忙。所以设计联合索引的时候,必须把区分度高的列放在最前面,其次考虑等值查询列、范围查询列的顺序,同时尽量让一个索引覆盖多条高频 SQL 的查询条件,这就是所谓的“一索引多用”。

前缀索引是处理长字符串列(比如 VARCHAR(255) 的文本字段)的常用手段。对整列建索引会占用大量空间,而且索引树的层级因为键值变长可能更高,检索效率下降。你可以只对列的前 N 个字符建索引,比如CREATE INDEX idx_phone_prefix ON users(phone(6))。但前缀索引有个明显的局限:它无法用于覆盖索引优化,因为索引里只保存了前缀字符,查询依然要回表取完整值。另外前缀长度的选择直接影响区分度,太短会导致你只拿到一大把相同前缀的索引项,过滤效果变差;太长又失去压缩意义。我的做法是先跑一条 SQL 统计不同前缀长度下的区分度,保留在可接受范围内最短的前缀长度。

3.3 实操案例:订单表索引怎么建才合理

讲完理论,给一个完整的实操案例。假设我在维护一个电商订单系统,订单表是orders,核心字段包括 id、order_no、user_id、status、create_time、total_amount。业务上高频查询有:用户查询自己的订单列表(WHERE user_id = ? ORDER BY create_time DESC)、后台按状态筛选订单(WHERE status = ? AND create_time BETWEEN ? AND ?)、根据订单号查询详情(WHERE order_no = ?)。

按照查询频率和区分度来设计:

ALTER TABLE orders ADD UNIQUE KEY uk_order_no (order_no), ADD KEY idx_user_create (user_id, create_time), ADD KEY idx_status_create (status, create_time);

uk_order_no服务订单号查询,同时保证唯一;idx_user_create用户维度查询订单列表时,索引天然有序,避免 filesort;idx_status_create支持后台的状态加时间范围筛选,注意状态列的区分度不高,但配合时间范围列之后过滤效果还是可以接受的。这个设计只用了三个索引,就覆盖了绝大部分高频查询场景,也不至于让写入压力过大。

这里补充一个区分度判断的 SQL:

SELECT COUNT(DISTINCT user_id) / COUNT(*) AS user_cardinality, COUNT(DISTINCT status) / COUNT(*) AS status_cardinality FROM orders;

区分度越接近 1,索引过滤效果越好。如果某个列的区分度低于 0.1,比如状态列只有几个枚举值,单列索引几乎没什么价值,一般要和其它列组合使用。

3.4 创建索引时的三个实操要点

第一个要点是尽量在业务低峰期在线添加索引,或者使用在线 DDL 工具。MySQL 5.6 之后 InnoDB 支持了ALGORITHM=INPLACE的在线 DDL,很多索引添加操作可以在线完成,不再完全锁表,但不同版本的实现细节有差别,5.7 和 8.0 的体验也不完全一样。生产环境已经跑到千万级的表,我建议先用ALTER TABLE ... ADD INDEX ... , ALGORITHM=INPLACE, LOCK=NONE确认支持情况,如果不支持就换用 gh-ost 或者 pt-online-schema-change 这类工具,避免长时间阻塞业务写操作。

第二个要点是时刻关注执行计划。索引创建完成后,用EXPLAIN验证 SQL 是否真正用到了索引,同时观察 type 字段(常见有ref、range、index、ALL)、key 字段、rows 字段。我之前遇到过一次很典型的翻车:索引建完了,EXPLAIN 显示 type 还是ALL,全表扫描。排查后发现是查询条件里对索引列做了隐式类型转换,比如字段类型是 varchar,查询参数却传了数字,MySQL 会自动把索引列转成数字去比较,索引自然失效。

第三个要点是关注索引大小和存储成本。一个索引建下去,占用的磁盘空间可不是一个小数。在数据量几十 GB 的库里,一个每列都是大 varchar 的联合索引,轻松吞掉好几个 GB。单独的索引占用空间可以这样查:

SELECT database_name, table_name, index_name, stat_value * @@innodb_page_size AS index_size_bytes FROM mysql.innodb_index_stats WHERE database_name = 'your_db' AND table_name = 'orders';

如果索引占用的空间和收益不成比例,那就得重新评估这个索引到底有没有存在的必要。

4. 删除索引:比创建更需要谨慎

4.1 两种删除方式的适用边界

删除索引有两种标准写法,效果基本一致。

方式一,使用DROP INDEX:

DROP INDEX idx_user_create ON orders;

方式二,使用ALTER TABLE ... DROP INDEX:

ALTER TABLE orders DROP INDEX idx_user_create;

两种方式没有本质差别,DROP INDEX语法上更加直观,ALTER TABLE则能在同时修改表结构时一并处理。我个人的习惯是:只删索引时用DROP INDEX,删索引加改字段混在一起时用ALTER TABLE,这样目的更明确,别人看你的变更脚本时也更容易一眼读懂。

有一个细节特别提醒:主键索引不能直接通过这两条语句删除。你要删掉主键,得先把主键约束处理掉,或者重建表。比如原来主键设计不合理需要更换,步骤是先把原主键 DROP 掉再添加新主键,但这个过程中涉及唯一约束、外键依赖等一系列问题,务必先梳理清楚依赖关系再操作。

4.2 什么时候你才需要删除索引

删除索引比创建索引更考验经验。我发现真正需要删除索引的场景往往伴随着业务变化或历史包袱。

第一种场景是冗余索引清理。这是最常见的删除需求。由于早期迭代时每个人各建各的索引,同一个列可能同时存在于多个单列索引和联合索引中。比如表上已经有联合索引(user_id, status),又单独在status上建了一个索引idx_status。从查询效率上讲,idx_status对于WHERE status = ?的过滤依然有效,但联合索引已经覆盖了大部分状态查询场景,单独的状态索引就是冗余的。冗余索引占空间、拖慢写入,白白消耗资源。

第二种场景是索引设计不合理,区分度过低。我曾经见过一张日志表,在level列上建了普通索引,而 level 一共只有 INFO、WARN、ERROR 三个值,查询时往往命中了大量数据还需要回表,实际效率甚至不如全表扫描。这种索引删掉不会带来任何负面影响,反而省出一块磁盘空间。

第三种场景是查询模式发生根本变化。比如某个列表查询原来按字段 A 频繁过滤,后来业务重构改成按字段 B 过滤,字段 A 的索引就变成了摆设。这种索引因为长期不再被使用,完全可以删掉。你可以通过performance_schema.table_io_waits_summary_by_index_usage这类视图统计索引的使用次数,一个索引如果长时间没有任何访问,就要考虑它是不是已经失去了存在价值。

4.3 删除索引的完整操作流程

生产环境删除索引,我一般遵循这样的步骤。第一步,先在测试环境确认删除这个索引对相关 SQL 的执行计划没有颠覆性影响。第二步,在预发布环境通过EXPLAIN和真实业务流量观察一段时间,尤其关注慢查询有没有增多。第三步,选业务低峰期执行删除语句,删除索引本身的操作通常比创建快,但还是建议在变更窗口内完成。第四步,删除后再次收集执行计划,确认全表扫描或文件排序没有大面积出现。

完整流程可以用下面的表单总结:

阶段动作关键检查点
前置分析查看索引使用统计、确认冗余关系该索引近期是否有读请求
预发验证在测试库执行 DROP,跑相关 SQL 对比 EXPLAINsql 是否出现 type=ALL 或 filesort
变更执行低峰期执行删除语句观察数据库线程和锁状态
事后观察持续收集慢查询日志和 performance_schema 数据是否出现新的慢查询告警

还有一类需要特别小心的场景:索引被外键约束引用。如果某个列上有外键,MySQL 通常会自动为它创建索引,你手动删除这个索引可能会直接报错或者导致外键约束失效。操作之前一定要用下面的语句检查一下约束情况:

SELECT table_name, column_name, constraint_name, referenced_table_name FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL AND table_name = 'orders';

4.4 删除索引时容易被忽略的锁问题

删除索引并不像很多人想象的那样是“瞬时完成、影响为零”的操作。在 InnoDB 里,DROP INDEX 会触发表的重建或者索引元数据的修改,具体行为取决于索引类型和 MySQL 版本。对于大表来说,删除索引同样会占用一定的 IO 资源和系统资源,在业务高峰期执行,照样可能引起性能抖动。

另外说一下隐藏索引的概念——MySQL 8.0 支持隐藏索引(invisible index)。这是一个非常实用的过渡方案:你可以先把索引设置为不可见(ALTER TABLE orders ALTER INDEX idx_user_create INVISIBLE),而不是直接删除。优化器会忽略隐藏索引,查询不再走它,但索引结构依然保留,万一发现删了之后业务查询变慢,一条语句就能立刻恢复可见。等观察期结束确认索引确实没用,再真正 DROP 掉。这个方式比直接删除稳妥得多,我在生产环境里遇到拿不准的索引,都先用隐藏索引试探底。

5. 索引实战:高频问题与避坑经验

5.1 索引失效场景速查表

索引建得好不好,不只看建了多少,还得看查询有没有真正命中。我整理了实践中最高频的索引失效场景,这张表可以直接在你排查性能问题时拿来对照。

问题场景原因解决思路
对索引列使用函数LOWER(column) 或 DATE(column) 导致优化器无法利用索引改写为范围条件,或给计算列建表达式索引
隐式类型转换varchar 列与数值比较统一参数类型
左模糊匹配LIKE '%keyword%' 无法用索引改为前缀匹配,或使用全文索引
OR 条件跨列WHERE a = 1 OR b = 2 时优化器可能放弃索引用 UNION 拆分或建关联索引
联合索引不符合最左前缀跳跃前导列调整查询字段或重新设计索引顺序
索引列参与运算WHERE num * 2 > 10 等改写到独立列比较

拿隐式类型转换举例,我当时排查一个线上慢查询:SELECT * FROM users WHERE mobile = 13800138000,mobile 是 varchar(11),传参是数字 13800138000。EXPLAIN 发现 type 为 ALL,索引完全没走。后来我把参数统一成字符串'13800138000',查询直接走了索引,执行时间从几百毫秒降到几毫秒。这类问题校验成本极低,却是生产环境里最常见的性能杀手之一。

5.2 二级索引更新时的锁顺序问题

索引不只是查询优化工具,它还会影响 InnoDB 的加锁行为,而这方面的坑往往比较隐蔽。在一次排查死锁问题的时候,我遇到过一个非常典型的场景:业务上有一个更新操作,UPDATE orders SET status = ? WHERE order_no = ?,因为 order_no 上有二级索引,InnoDB 会先锁二级索引项,再回表去锁聚簇索引对应的主键行;与此同时,另一个事务通过主键 id 更新同一行,顺序则可能相反——先锁聚簇索引行,再锁二级索引项。两个事务各自持有了一把锁,又都在等对方释放另一把锁,形成交叉等待,最终造成死锁。

这种现象的根本原因在于,InnoDB 对索引的锁操作是逐条执行的,而不是一次性把所有锁都拿完。它的锁顺序依赖索引的扫描顺序,当多个事务通过不同的索引路径访问同一行数据时,加锁顺序就可能不一致。处理思路一般是:尽量让高频更新操作使用同一条索引路径(比如都通过主键更新),减少跨索引更新的概率;或者使用SELECT ... FOR UPDATE提前按统一顺序加锁,给并发事务建立稳定的锁获取次序。建议把这个场景写进你的代码审查清单里,凡是涉及“通过二级索引更新数据”的写操作,都要考虑锁顺序带来的死锁风险。

5.3 主键索引设计的常见误区

主键索引的叶子节点就是整行数据,这个特殊性决定了主键设计的容错率极低。最常见的误区有两类:一类是使用业务字段作主键,比如用身份证号或订单号;另一类是用随机的 UUID 作主键。

业务字段作主键的问题在于:主键必须唯一且稳定,业务字段一旦发生变更,代价极高;而且业务字段往往不是递增的,Insert 时数据页的分裂现象会更频繁,影响写入效率。UUID 作主键同样糟糕——它不保证顺序性,插入时索引页需要频繁分裂与重排,数据存储碎片化明显,B+ 树的性能优势被削弱得很厉害。更推荐的做法是使用自增整数或者雪花算法生成的分布式有序 ID 作为主键。自增主键每一次插入都是追加写,顺序性和写入性能都最好,如果分布式的场景不允许依赖数据库自增,雪花 ID 这种趋势递增的方案也过得去。

强调一个设计原则:主键越短越好。主键会出现在每一个二级索引的叶子节点上,主键越长,二级索引的体积就越大,占用的缓冲池内存也越多,磁盘 IO 负担随之上升。这也是为什么在很多表设计中,BIGINT AUTO_INCREMENT比 VARCHAR 类型主键更受青睐的原因。

5.4 索引表空间回收与表重建

创建索引会占用表空间,删除索引后空间能不能立刻还给操作系统?答案是不能立刻。InnoDB 的表空间文件(比如orders.ibd)在删除了索引之后,会留下碎片空间,文件大小不会自动缩减。如果你特别在意磁盘占用,需要执行表重建操作来整理和回收空间。

重建表最简单的方式是:

ALTER TABLE orders ENGINE=InnoDB;

这条语句的作用是让 InnoDB 重建整张表,在重建过程中重新组织数据和索引,清掉碎片空间。要注意这个过程在 MySQL 5.7 之前会锁表,即使在 5.7 和 8.0 支持了在线 DDL,执行时依然有额外 IO 和主从延迟的风险。还有一个备选方案是OPTIMIZE TABLE orders,它的效果实际上也是重建表。对于超大表,这个操作的耗时可能非常长,务必避开业务高峰期,并且提前在磁盘空间上留足余量。

另一种做法是直接重建索引,通过删掉再重新创建的方式整理索引页的碎片:

ALTER TABLE orders DROP INDEX idx_user_create, ADD INDEX idx_user_create (user_id, create_time);

这条语句的代价是重建索引期间有 IO 压力,但相比整表重建要轻量不少。实测下来,如果只是想消除索引空间的碎片,这种“删除+重建”的做法已经足够,没必要整表重建。

5.5 用隐藏索引做删除前的“灰度验证”

前面提到隐藏索引,这里补充一个具体的操作示例,因为它真的能帮你避免很多事故。假设我怀疑idx_status_create已经基本没有查询在用了,但后台系统偶尔可能有一次性报表任务还在跑,我不确定删除它会不会导致这批任务变慢。这时候我会这样做:

-- 第一步:让优化器忽略这个索引 ALTER TABLE orders ALTER INDEX idx_status_create INVISIBLE; -- 第二步:观察慢查询日志,确认这段时间有没有因为索引隐藏而变慢的 SQL -- 第三步:确认安全之后,彻底删除索引 DROP INDEX idx_status_create ON orders;

整个流程下来,如果中间哪一步发现问题,一条ALTER TABLE orders ALTER INDEX idx_status_create VISIBLE就能立刻把索引救回来,比删除之后又后悔要安全太多了。唯一要注意的是INVISIBLE这种索引状态在 MySQL 8.0 才支持,5.7 及之前的版本没有这个能力,有条件的话建议尽早把生产环境升到 8.0。

6. 关于索引维护,最后想说的几点经验

从建索引到删索引,整个循环其实就是一个“理解业务查询模式”的过程。索引不是堆得越多越好,也不是建了就一劳永逸——它是一个需要持续维护的数据库对象。表结构会变,业务查询会变,数据量会变,索引的方案也必须跟着演进。

我个人在实操中最受益的几个习惯,再单独强调一遍:第一,每次 DDL 变更之前一定先看EXPLAIN,执行计划跑不出预期就坚决不动手;第二,建立一套索引使用情况巡检机制,定期看performance_schema里各个索引的访问统计,把长期闲置的索引找出来;第三,删除不确定是否还有用的索引时,优先用隐藏索引做验证,而不是一刀切;第四,所有涉及索引的变更脚本都要走版本管理,上线时和代码发版一样谨慎,操作时间窗口避开业务高峰。

我之前踩过不少坑,最深刻的一条是:索引优化不能只靠文档和经验,一定要基于你线上的真实数据和真实查询。同样一张表,同样的索引设计,在不同业务量、不同数据分布下的表现可能天差地别。先收集数据、再分析查询、最后才动手变更,这个顺序永远不要颠倒。

如果你正在处理某张具体表的索引问题,建议从慢查询日志开始,把消耗最高那几条 SQL 拿出来,逐个用EXPLAIN分析,你会发现真正需要创建的索引其实没有几个,真正该删除的索引反而经常被忽略。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询