先聊聊我自己的感受。我刚工作那会儿,以为“建索引”就是把热门查询字段挨个加上 index,结果生产环境一张千万级订单表的查询慢到报警,加了索引反而写入变慢、磁盘飙升,最后被架构师拉着看了半天 explain 才明白,索引这东西远不是“加就完了”。MySQL 里的索引,表面看是加快查询的数据结构,往深了说,它直接影响表的物理存储方式、写入吞吐、查询计划,甚至整个数据库的生命周期。这篇文我不打算给你堆概念目录,而是从一条慢查询的真实场景开始,把主键索引、二级索引、联合索引、最左前缀、覆盖索引、索引下推、索引失效这些事,用我能讲到的最直白的方式过一遍,顺便把生产环境踩过的坑一起交代了。
1. 先搞清楚:索引到底帮我们省了什么
1.1 一次慢查询的排查,让我想通了索引的价值
之前维护过一个订单系统,有个表叫 order_info,数据量大概一千万行。某天线上反馈后台订单查询接口超时,我看了一下慢查询日志,发现有一条 SQL 平均耗时三秒多:
sql复制SELECT *
FROM order_info
WHERE user_id = 123456
ORDER BY create_time DESC
LIMIT 10;
这条 SQL 看起来平平无奇,user_id 上也建了索引,怎么会慢?我执行 EXPLAIN 一看,possible_keys 里有 idx_user_id,但 key 却是 NULL,type 显示 ALL,也就是全表扫描。当时我第一反应是“优化器是不是傻了”,后来查了数据分布才发现,user_id = 123456 这个用户的订单量几十万行,占了整表相当比例,优化器算了笔账,觉得走二级索引再回表,还不如全表扫一遍然后排序快。
这就是索引的第一个真相:索引不是越建越快,而是帮你减少数据扫描范围。全表扫描要读一千万行,走了索引但命中数据量太大时,回表成本叠加起来可能更贵,优化器当然会“叛变”。而真正理想的索引设计,是让查询的扫描行数足够少,少到优化器哪怕闭着眼睛也知道该选它。
从这个案例能引出一个特别重要的判断标准:看一个索引建得好不好,不是看 SQL 里“有没有用到索引”,而是看扫描行数下降了几个数量级,以及有没有额外的排序、回表、临时表操作。 如果一条 SQL 扫了 500 万行才出 10 条结果,即便走索引也没意义,本质上还是等于在灾难现场打补丁。
1.2 用翻书目录来理解B+树索引
讲索引原理,很多文章喜欢堆“平衡树”“多路搜索”这些术语,听着很吓人。我更喜欢拿《新华字典》做比喻:字典正文是按拼音排序的,如果你想找一个读音是“suo”的字,有两种办法,第一种从第一页翻到最后一页,第二种先看目录页找到“s”所在的大范围,再顺着“suo”精确锁定页码,然后翻过去。
MySQL 的索引就是那套目录。但它的目录不是简单的“音序 -> 页码”两层结构,而是一棵从根节点到叶子节点的多级 B+ 树。最底层的叶子节点按照索引列的值排好序,上层节点存储“某个范围的最小值/最大值”和对应的子节点指针,方便快速定位。
为什么目录必须“排好序”?因为只有排好序,才能做二分查找。你翻字典找字,不需要从每个字开头看;数据库沿着 B+ 树往下走,每次比较都能砍掉一大半数据,这也是为什么 B+ 树能把千万级数据的查找次数压到三到四次磁盘 IO。所以索引的本质一句话:用额外的有序数据结构,换查询时的快速定位。
这里要先破一个误区,有不少人觉得“索引越多越好”,真的不是。每建一个索引,写入数据时都要额外维护一棵 B+ 树的排序和变更,插入一条记录可能要同时更新好几个索引。如果你的业务是高频写入场景,索引建太多会导致磁盘 IO 和 CPU 开销明显上升,表面上查询快了,整体吞吐却掉了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么InnoDB选B+树当索引结构,而不是红黑树或哈希
2.1 磁盘读数据的物理瓶颈:随机IO有多慢
要理解 B+ 树,得先理解数据库最大的敌人——磁盘随机 IO。一个机械硬盘随机读写一次大概要 5 到 10 毫秒,SSD 在几十微秒到几百微秒量级,听起来不大,但要知道 CPU 处理一条指令是纳秒级的。数据库里动辄几百万行数据,如果每查一个值都要满盘乱转,系统早就卡死了。
所以数据库设计的第一准则,就是尽量减少磁盘访问次数。二叉搜索树(BST)能不能当索引?理论行,但它最差情况下会退化成链表,查找次数等于树高。红黑树是平衡的,树高大概是 log2(N),一千万数据大约 24 层,也就是最多 24 次磁盘 IO,太深了。而且红黑树每个节点只存一个键值,相邻节点的数据在磁盘上往往不连续,这就会导致大量的随机 IO。
B+ 树厉害在哪?它是一个节点里存储多个键值,每个节点对应磁盘上一个页(默认 16KB),树的高度通常只有 3 到 4 层。一千万数据、每个节点能存上百个键时,三层树就能装下,查找时最多读 3 到 4 个页,也就是 3 到 4 次 IO,代价完全可控。
2.2 红黑树、B树、哈希、B+树:谁更适合当索引
很多人分不清 B 树和 B+ 树,简单说:B 树每个节点既有索引键又有数据(或者数据指针),而 B+ 树只有叶子节点存数据,非叶子节点只存键值和子节点指针。这带来了几个关键差异。
- B+ 树的非叶子节点能存更多键,树更矮,磁盘 IO 更少;
- B+ 树的叶子节点之间用双向链表连接,排序、范围查询、
ORDER BY、分页都更友好; - B+ 树的所有叶子节点都在同一层,查询任意数据的耗时非常稳定。
而哈希索引呢?InnoDB 其实有自适应哈希索引,但它是自动维护的,人不能直接创建。哈希索引的优点是等值查询 O(1),可它致命缺点是不支持范围查询、不支持模糊查询、也不支持排序。你在业务里绝大多数查询都不是简单的等值,所以 InnoDB 默认的索引结构必须是 B+ 树。
我整理了一张对比表,方便你直观感受:
| 结构 | 等值查询 | 范围查询 | 排序 | 树高 | 适用场景 |
|---|---|---|---|---|---|
| 哈希表 | 极快 | 不支持 | 不支持 | - | 等值短查询(如主键直接命中) |
| 二叉树 | 快 | 支持 | 支持 | 高,稳定性差 | 不适用磁盘存储 |
| 红黑树 | 较快 | 支持 | 支持 | 约 log2(N),仍偏高 | 内存索引 |
| B树 | 快 | 支持 | 支持 | 较低 | 文件系统、部分数据库 |
| B+树 | 快 | 支持 | 支持 | 3~4 层 | InnoDB 默认 |
从表里能看到,B+ 树是“全都要”的综合优等生。范围查询靠叶子节点的链表,等值查询靠树层级定位,排序不需要额外做 filesort,因为叶子节点天然有序。
2.3 主键为什么推荐自增而不是UUID
这个问题几乎是面试必问,也是设计主键索引时必须想明白的。InnoDB 的聚簇索引(主键索引)叶子节点存的是整行数据,而且表本身的物理存储顺序就是按主键排序的。如果主键是自增整数,新插入的行会在末尾追加,已经写满的页不需要频繁重新分配。但如果主键是 UUID 这种随机字符串,新主键可能落在任意位置,如果目标页满了,就得做“页分裂”操作——把中间数据拆到新页,还要修改父节点的指针。页分裂既慢又产生碎片,时间久了整个表性能会明显下滑。
另外,二级索引的叶子节点保存的是主键值。主键越长,每个二级索引就越大,占用磁盘空间越多,缓存命中率越低。UUID 是 36 个字符,跟 8 字节的 bigint 相比,完全不是一个量级。所以生产设计我尽量用 bigint unsigned 自增主键,除非业务明确需要全局唯一字符串做表面 ID,那也要尽量用短格式。
3. 聚簇索引和二级索引:回表到底在干什么
3.1 InnoDB表本身就是一棵B+树
很多刚接触 MySQL 的人会下意识认为“表是表,索引是索引,两者分开”。InnoDB 里这两者是合在一起的:整张表的数据就是一棵以主键为排序键的 B+ 树,我们也叫它聚簇索引。也就是说,主键索引的叶子节点里存的不是“主键值 + 行指针”,而是完整的一行数据。
这也解释了几个常见规则:
- 为什么 InnoDB 表必须有主键?如果没有显式主键,它会找第一个非空唯一索引,找不到就生成一个隐藏的 6 字节 rowid。
- 为什么二级索引的查询通常比主键索引慢?因为二级索引叶子节点存的是主键值,查到主键后还得再去聚簇索引里把整行数据捞出来,这就是回表。
所以在 MySQL 里,建表时有没有主键、主键选得好不好,直接影响所有索引的存储成本和查询效率,这不是小事。
3.2 二次查询:一条SQL的完整路径
看一条常见查询:
sql复制SELECT * FROM user WHERE phone = '13800138000';
假设 phone 上建了二级索引 idx_phone,执行过程是:
- 在
idx_phone这棵 B+ 树里,按phone = '13800138000'找到对应叶子节点,取出主键id; - 用这个
id去聚簇索引 B+ 树里找完整行记录; - 返回所有字段。
第 2 步就是回表。如果查询只需要 phone 和 id 两个字段,其实第 2 步可以跳过去,二级索引的叶子节点已经包含了这两个值,不需要看完整行。这就是后文要展开的覆盖索引。理解回表,是理解 MySQL 查询优化的一把钥匙:很多慢查询不是没走索引,而是走了二级索引后回表太多次。比如 IN (1,2,3,...1000) 一批主键回表,可能比直接全表扫还慢。
3.3 主键短一点,二级索引就瘦一圈
聚簇索引的叶子节点存整行,这个改不了;二级索引的叶子节点存主键值,这可是可以被我们影响的。假如一张表有 6 个二级索引,主键从 8 字节 bigint 换成 36 字节字符串,光索引的额外空间可能增加几百 MB,对大表来说非常可观。
另一个知识点是主键顺序对顺序扫描也很关键。自增主键让数据在磁盘上趋向于顺序追加,做范围扫描(比如按时间或 ID 排序)时预读效率更高;UUID 主键让数据随机分布,范围扫描基本等于随机 IO。
所以哪怕只是为了索引体积,我也强烈建议:InnoDB 表主键选占字节少的自增整数,能用 int unsigned 就不用 bigint,能用 bigint 就别用字符串。
4. 联合索引和最左前缀:面试常考,工作也最容易翻车
4.1 联合索引的内部排序逻辑
联合索引就是多个列组成的索引,比如 idx_user_create (user_id, create_time)。很多人以为联合索引是“单独对 user_id 建索引,再单独对 create_time 建索引”,其实不是。
联合索引的底层仍然是一棵 B+ 树,只不过叶子节点先按第一个字段排序,第一个字段相同的记录再按第二个字段排序,依此类推。拿 (user_id, create_time) 来说,它的排序规则是:先按 user_id 排,相同 user_id 内部再按 create_time 排。
这个结构决定了:如果查询条件里只用 create_time,而没用 user_id,那么这个联合索引大概率帮不上忙。因为整棵树第一排序键是 user_id,你想直接按 create_time 找,就像一本先按姓氏、再按名字排序的电话簿,你只知道名字,没法直接快速定位。
这就是“最左前缀原则”的出处:联合索引能用到的最左连续前缀。对 (a, b, c) 这个索引,a、a,b、a,b,c 都能走,但单独用 b 或 c 走不了。
4.2 哪些组合能走索引,哪些只能干瞪眼
假设有联合索引 idx_abc (a, b, c),我们模拟一些查询条件:
sql复制WHERE a = 1; -- 走索引,用 a 前缀
WHERE a = 1 AND b = 2; -- 走索引,用 a,b
WHERE a = 1 AND b = 2 AND c = 3; -- 走索引,用 a,b,c
WHERE b = 2 AND c = 3; -- 无法走该索引(没有 a)
WHERE a = 1 AND c = 3; -- 能用 a 前缀,c 没法直接在树里精确定位
WHERE b = 2 AND a = 1; -- 优化器会调整顺序,等价于 a=1 AND b=2,能走
注意最后一条,SQL 的书写顺序不影响优化器判断,它会把常量条件重排成符合最左前缀的顺序。所以别以为“把 b 写在前面就一定能用”,真正决定权在优化器手里。
有个细节容易忽略:WHERE a = 1 AND c = 3 时,索引会走,但只用到了 a 这一列。c = 3 能不能用上,要看 MySQL 版本和是否开启索引下推,5.6 之后的 Index Condition Pushdown 可以在存储引擎层用 c 做过滤,减少回表行数,后面专门讲。
4.3 范围查询后面的列为什么失效
这是联合索引最经典、也最容易踩坑的问题。还是 idx_abc (a, b, c),执行:
sql复制WHERE a > 100 AND b = 5
很多人的直觉是“索引不是有 b 吗?b=5 肯定能用”。但实际是:在 B+ 树里,所有记录先按 a 排序,a > 100 是一个范围,范围里的 b 值不再是全局有序的。举个例子,a=101 的记录里 b 可能是 1,5,9;a=102 的记录里 b 也可能是 1,5,9。你想精确锁定 b=5,但 b 在这个范围内是分散的,没法继续用二分查找往下定位。
所以结论是:联合索引里,遇到范围查询(>、<、BETWEEN、LIKE 前缀匹配也算范围的一种)之后,后面的列索引生效能力会打折扣。这也是为什么设计联合索引时,要把高频的等值条件放在前面,把范围条件放到最后。比如 (user_id, create_time) 就比 (create_time, user_id) 更适合“查某人的某段时间订单”这种场景。
如果真的有 a > 100 AND b = 5 这种查询,可以考虑把 b 也放到 a 前面去,改成 (b, a),但也要权衡另一个高频查询是不是用 a 前缀。联合索引设计本质就是吃一个“不同查询模式”的平衡,不存在银弹。
4.4 把区分度高的列放前面,还要看查询频率
联合索引的列顺序,第一个考虑原则是等值条件的列,第二个考虑原则是区分度。区分度可以用 COUNT(DISTINCT col) / COUNT(*) 来衡量,值越接近 1,说明这列的取值越分散。把区分度高的放前面,能让 B+ 树在每个层级砍掉更多数据。
不过,“区分度高”不是唯一标准。比如一个 status 列,区分度只有 0.01(大量重复),如果业务里 90% 的查询都拿 status 当等值条件,那也不得不把它放进联合索引,而且尽量放在靠前位置。你可以把它理解为“高频条件优先,其次是区分度”,两者冲突时,高频条件经常更重要。
5. 覆盖索引和索引下推:不花钱就能让性能翻倍
5.1 覆盖索引:查询列缩进索引里,回表直接省掉
覆盖索引,指的是查询需要的所有字段都已经包含在同一个索引里,不需要再回表取完整行。一个最简单的例子:
sql复制SELECT id, phone FROM user WHERE phone = '13800138000';
如果你只建了 idx_phone (phone),二级索引的叶子节点其实存了 (phone, id),这个查询压根不需要回聚簇索引,EXPLAIN 的 Extra 列会显示 Using index。
覆盖索引的收益在“大表 + 高频查询”场景下非常恐怖。比如订单列表页只需要展示订单号和金额,不需要全字段,这时建一个 (user_id, order_no, amount) 的覆盖索引,查询可以完全在一棵二级索引里完成,省掉成千上万次回表。
但注意,覆盖索引不是越多越好,因为索引字段越多,存储和写入开销越大。我的经验是:优先优化最核心、最高频的 3 到 5 条查询,让它们达到覆盖索引效果,而不是给所有字段都加索引。可以用 EXPLAIN 看 Extra 列,如果出现 Using index condition 且 rows 很大,就要考虑能不能换成覆盖索引。
5.2 索引下推:让存储引擎多做点活
索引下推(Index Condition Pushdown,ICP)是 MySQL 5.6 引入的优化,它解决的就是“联合索引在某些条件下无法完全匹配”时的回表浪费问题。
回到前面 idx_abc (a, b, c) 的例子:WHERE a > 100 AND b = 5。在没有 ICP 时,存储引擎沿着二级索引把 a > 100 的所有主键全部回表,然后在 server 层再过滤 b=5,大量无效回表。
有了 ICP 后,存储引擎在读取二级索引时,可以在索引内部先判断 b=5,过滤掉明显不符合条件的主键值,只回表剩下的少量记录。注意“判断 b=5 是在二级索引里做的”,不用回表也能看到 b 的值,因为联合索引本身就包含 b 字段。所以 ICP 是一个“减少回表次数”的优化,但并不等于索引完全匹配上了 b 列。
sql复制-- Extra 里出现 Using index condition 就表示用了 ICP
EXPLAIN SELECT * FROM t WHERE a > 100 AND b = 5;
需要说明的是,ICP 对 InnoDB 和 MyISAM 都有效,但它不能减少排序成本、也不能把范围查询变成等值匹配。它在实际生产中最常见的价值,是让“范围条件后面的等值条件”尽可能发挥过滤作用,把回表量压缩一个数量级。
5.3 一次慢SQL的优化对比
拿真实的订单查询举例,优化前:
sql复制SELECT order_id, amount, status
FROM order_info
WHERE user_id = 123456
AND create_time BETWEEN '2024-01-01' AND '2024-06-01'
AND status = 1;
当时表里只有 idx_user_id (user_id) 一个索引,执行计划是:先用 user_id 查出该用户所有订单,回表拿完整行,再在 server 层过滤 create_time 和 status。这个用户订单很多,回表行数高得吓人,查询要一秒多。
优化方案是建联合索引:
sql复制ALTER TABLE order_info
ADD INDEX idx_user_create_status (user_id, create_time, status);
由于 user_id 是等值条件,放在最前;create_time 是范围条件,放在中间;status 是等值条件,按理说放在范围条件后面无法精确定位,但配合 ICP,存储引擎读取二级索引时能直接对 status 做过滤,回表行数大幅下降。再者,查询需要的字段 (order_id, amount, status) 其中 order_id 是主键,二级索引叶子节点天然带主键,status 是索引列,但 amount 不在索引里,所以依然要回表拿 amount,不过回表量已经降到很小。
再看一个完全避免回表的案例,把高频查询改成:
sql复制SELECT order_id, status
FROM order_info
WHERE user_id = 123456
AND create_time BETWEEN '2024-01-01' AND '2024-06-01'
AND status = 1;
这时 order_id(主键)和 status 都在索引里,Extra 会变成 Using where; Using index,可以做到零回表。如果业务确实高频只需要这两个字段,那就把 amount 也加进索引做成覆盖索引,连回表都省了。不过每次加字段都要想清楚:这棵索引会不会因为字段太长而失去优势。
6. 索引失效场景:这些坑我基本都踩过
6.1 隐式类型转换:乱传参数的代价
索引失效第一大坑,是字段类型和查询参数类型不一致导致隐式转换。我在一个用户表上见过这种情况:
sql复制-- phone 字段是 varchar(20),但业务代码传了一个数字
SELECT * FROM user WHERE phone = 13800138000;
MySQL 会把字符串和数字比较时,隐式把字符串转成数字再比较。问题是,对索引列做函数/运算,会让索引列不再保持原值,B+ 树直接没法利用了。这里的隐式转换本质上是对 phone 列套了一层类型转换,所以就会全表扫描。
正确写法是传字符串:
sql复制SELECT * FROM user WHERE phone = '13800138000';
这种坑用肉眼很难发现,我建议养成执行 EXPLAIN 的习惯,看到 type 从 ref 退化成 ALL 时,优先怀疑字段类型和参数类型不一致。
6.2 函数运算:一包函数,索引直接罢工
还有一类非常隐蔽的问题,是在索引列上包函数。比如:
sql复制SELECT * FROM order WHERE DATE(create_time) = '2024-06-01';
即使 create_time 有索引,DATE() 函数也会让索引失效,因为函数的结果需要实时计算,无法直接走 B+ 树排序。比较推荐改成范围查询:
sql复制SELECT * FROM order
WHERE create_time >= '2024-06-01 00:00:00'
AND create_time < '2024-06-02 00:00:00';
同理,在 WHERE 里对索引做算术运算也会失效:
sql复制-- 不走索引
SELECT * FROM user WHERE age + 1 > 30;
-- 可以走索引
SELECT * FROM user WHERE age > 29;
这里有一个判断原则:看索引列是否保持“裸列”状态。如果条件里索引列参与了函数、计算、类型转换,大概率会失效。
6.3 前导模糊查询和OR:看起来无辜,其实很伤
LIKE 查询是模糊匹配的常用手段,但不是所有 LIKE 都会失效:
sql复制SELECT * FROM user WHERE name LIKE '张%'; -- 走索引,前缀匹配可以
SELECT * FROM user WHERE name LIKE '%张'; -- 不走索引,因为不知道以什么开头的
SELECT * FROM user WHERE name LIKE '%张%'; -- 不走索引
原因还是 B+ 树的有序性:前缀匹配能利用字符串前缀排序,而后缀或中间匹配无法在有序结构里定位。如果是 %张% 这种高频需求,业务量小没关系,业务量大就应该考虑全文索引或者搜索引擎,而不是让 MySQL 硬扛。
OR 也是一个经典陷阱:
sql复制SELECT * FROM user WHERE name = '张三' OR phone = '13800138000';
如果 name 和 phone 都有索引,优化器可能用 index_merge 分别走两个索引再合并结果,但很多情况下,它会把 OR 转换成全表扫描,尤其当多条件叠加、索引合并代价过高时。稳妥做法是把 OR 拆成 UNION ALL 两条查询,或改成 IN 等值集合。不过也要注意,如果两个字段都有索引且数据分布均匀,EXPLAIN 里出现 Using union(idx_name, idx_phone) 时,也可以保留 OR,具体以实际执行为准。
6.4 优化器宁愿全表扫,很多时候是它算过账的
我在第一节提的那个 user_id = 123456 案例,其实就是优化器“精打细算”的结果。当索引命中比例过高时,回表次数太多,优化器会认为全表扫描反而更便宜。这种情况最典型的表现是:possible_keys 里有索引,key 却是 NULL。
解决办法不是强迫它走索引,而是让查询范围更小。比如改成“最近三个月内的订单”,或者改成分页分批处理。如果业务场景实在需要扫大量数据,那说明索引设计的方向就得调整——比如用覆盖索引提升扫描效率,或者考虑建立更符合该查询模式的联合索引。
另一个容易忽略的点是统计信息过期。如果表数据大幅变更后没及时更新统计信息,优化器可能按错误的数据分布做判断。生产环境大表可以定期执行 ANALYZE TABLE,让它重新估算索引基数,这个操作比很多人想象中重要。
7. 前缀索引和索引设计:做了几年DBA才摸清的门道
7.1 前缀索引怎么算区分度
前缀索引,就是对字符串列的前 N 个字符建索引,而不是对整个字符串建索引。它非常适合长字符串列,比如邮箱、URL、身份证号。
使用方法:
sql复制ALTER TABLE user ADD INDEX idx_email_prefix (email(10));
这样做能显著压缩索引体积,但代价是:前缀索引不能用做覆盖索引,也不能精确排序。所以什么时候值得用?当列很长、区分度又主要集中在前几个字符时,收益最大。
如何确定前缀长度?最直接的办法是算区分度:
sql复制SELECT
COUNT(DISTINCT LEFT(email, 5)) / COUNT(*) AS diff5,
COUNT(DISTINCT LEFT(email, 8)) / COUNT(*) AS diff8,
COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS diff10,
COUNT(DISTINCT email) / COUNT(*) AS diff_all
FROM user;
我的经验是:选择足够接近 diff_all 的最小前缀长度。比如 diff8 = 0.9823,diff10 = 0.9988,diff_all = 0.9990,那就选 10。选太长失去压缩意义,太短区分度不够,会造成大量重复索引扫描。
7.2 冗余索引和重复索引:如何用explain找出来
很多表都是从业务初期一路给字段加索引加出来的,时间久了隐藏的冗余索引特别多。比如先建了 idx_user_id (user_id),后来为了新查询建了 idx_user_create (user_id, create_time),那 idx_user_id 其实就是冗余索引,因为 idx_user_create 的最左前缀已经覆盖了 user_id。
判断方法最简单粗暴:看联合索引最左边的列,是否已经存在单独的索引,如果是,单独那个大概率可以删。写成 SQL 不容易,但可以靠 information_schema.STATISTICS 表辅助。
另一个常见问题是重复索引,完全相同的字段和顺序建了两遍,纯属事故。清理这类索引能直接减少写入开销和磁盘占用。
7.3 一张可以抄作业的索引设计清单
做了这么多年,我把索引设计的经验总结成一份“抄作业”清单,每次新表上线前对着过一遍:
- 表必须有主键,尽量用自增
bigint或int,不用 UUID; - 每个表索引数量控制在 5~6 个以内,写入密集的表更要少;
- 联合索引的列顺序:等值条件优先,范围条件往后放,高频查询字段往前放;
- 优先为高频查询建立覆盖索引,减少回表;
- 长字符串列考虑前缀索引,并计算区分度确定长度;
- 避免在索引列上使用函数、运算、隐式类型转换;
- 用
EXPLAIN检查慢 SQL,重点看type、key、rows、Extra; - 定期用
information_schema检查冗余索引和重复索引。
这份清单不一定能解决所有极端性能问题,但至少能避开 90% 的常见索引陷阱。
8. 说点自己的体会
索引这个东西,刚接触时觉得是一堆规则,干得久了会发现它其实是一套关于“物理存储、排序结构、成本估算”的博弈。索引建得好不好,直接影响查询快不快,也影响写入稳不稳。我见过很多开发同学一遇到慢查询就加索引,结果越加越多,反而把数据库拖垮;也见过有人为了“避免索引失效”把 SQL 写得很别扭,最后优化器一样不领情。
我的真实建议是:不要背“哪些场景索引失效”的死结论,而是去理解为什么失效。一旦你理解了 B+ 树的有序性,理解了回表成本、覆盖索引、索引下推,你就自己能推断出哪种写法能走索引、哪种写法会被全表扫。遇到慢 SQL,先 EXPLAIN,再看 rows 和 Extra,最后才决定加不加索引、怎么调整索引列顺序。熟练之后,你会发现自己不需要再依赖“经验清单”,一眼就能看出问题在哪。
