MySQL索引类型详解:从B+Tree到慢查询优化的实战指南
1. 索引类型全景图先分清分类维度再谈选型聊 MySQL 索引以前先要摆正一个观念网上能搜到“MySQL 索引有哪几种类型”答案五花八门普通索引、唯一索引、主键索引、全文索引、哈希索引、联合索引、覆盖索引全混在一起。其实这些名词是从不同维度在描述索引不是同一级的分类。从数据结构维度看MySQL 常见索引有 BTree 索引、哈希索引、全文索引倒排索引、R-Tree 空间索引从逻辑功能维度看有普通索引、唯一索引、主键索引、全文索引、空间索引从字段个数维度看有单列索引和联合索引复合索引从物理存储维度看InnoDB 里又分成聚簇索引主键索引和二级索引辅助索引。很多人一上来就背“有哪些索引”结果面试一问“为什么主键索引快、普通索引慢”就卡壳原因就是没理解这些分类是交叉的。比如主键索引既是逻辑上的唯一索引又是物理上的聚簇索引底层结构还是 BTree。文章后面我会把所有维度串起来讲清楚你就不会再懵了。这篇内容适合三类人刚学数据库的初学者想建立完整索引知识框架工作中要被 SQL 慢查询折腾的开发运维准备面试需要系统性复盘索引原理的候选人。我会结合真实建表场景、Explain 实际结果、以及我这些年踩过的坑来讲保证你能直接照着用。2. 索引底层数据结构为什么偏偏是 BTree2.1 BTree 比二叉树、哈希强在哪数据存储以页为单位。MySQL InnoDB 默认一页是 16KB磁盘读写的最小单位和内存交换单位都是页。BTree 的设计是让非叶子节点只存索引键值和子节点指针一个 16KB 的页可以放下大量键值。假设一个索引键是 8 字节页指针是 6 字节那么一个页大约能存 16 * 1024 / 14 ≈ 1170 个键值对树的高度通常就 2 到 4 层。换句话说查找 1000 万行数据可能只需要 3 到 4 次磁盘 I/O。对比一下其他结构二叉搜索树数据有序插入时会退化成链表树高变成 N查找等于全表扫描。红黑树虽然能自平衡但树高在数据量大时依然有 logN 级别同样数据量下比 BTree 高得多磁盘 I/O 次数多。哈希索引等值查询 O(1)但底层是哈希表无法范围查询也无法排序。BTree 还有一个关键特性叶子节点之间通过双向链表相连。这意味着范围查询比如WHERE age BETWEEN 20 AND 30找到第一个满足条件的叶子页之后可以顺着链表往后扫不需要回溯到父节点重新遍历。这正是数据库里范围查询最频繁的底层支撑。2.2 聚簇索引和二级索引回表问题的根源InnoDB 中主键索引就是聚簇索引它的叶子节点直接存放整行数据。而二级索引普通索引、联合索引等的叶子节点存放的是索引键值 主键值。所以当你用一个普通索引查数据时过程通常是两步在二级索引的 BTree 里定位到对应键值拿到主键 ID。拿这个主键 ID 再去聚簇索引里查一次取出完整行数据。这个第二步就叫回表。回表不是必然发生的如果查询需要的列恰好都包含在索引里就不需要回表这叫覆盖索引。理解聚簇索引和二级索引的区别就能解释很多索引优化技巧的来源。注意InnoDB 表没有显式主键时会找第一个非空的 unique 键作为聚簇索引都没有就生成一个隐藏的 row_id 作为聚簇索引。所以“没有主键”并不是没有聚簇索引只是你没看到它而已。3. 按逻辑功能分类六种索引实战拆解3.1 普通索引、唯一索引、主键索引的取舍普通索引KEY/INDEX最基础的索引不限制键值是否重复目的就是为了加速查询。建表时写INDEX idx_name (name)就是普通索引。唯一索引UNIQUE KEY在普通索引基础上加了唯一约束键值不能重复但允许 NULL并且 NULL 可以有多个。唯一索引有两个作用一是保证业务数据的唯一性比如用户手机号、订单号二是能帮优化器做更多优化因为它知道最多只有一条匹配记录查到就能停。主键索引PRIMARY KEY唯一索引的特殊形式且不允许 NULL。在 InnoDB 里它还承担聚簇索引的角色。选主键时我非常推荐用自增 ID 或者有序的雪花 ID因为聚簇索引在插入时是顺序追加的页分裂概率小写性能稳定。如果用了 UUID 这种随机字符串当主键每次插入都可能在不同位置触发页分裂会产生碎片写入性能明显下降。实战建议普通索引和唯一索引选择上只要业务上字段需要唯一就直接建唯一索引别为了图省事只建普通索引然后在应用层做判断。数据库层的唯一约束是最可靠的应用层并发判断永远有漏洞。3.2 联合索引与最左前缀原则联合索引也叫复合索引是多个字段构成的索引比如INDEX idx_name_age (name, age)。很多人以为建了联合索引等于是给每个字段都建了索引这个理解是错的。联合索引底层是一棵 BTree排序规则是先比第一个字段相同的再比第二个字段依此类推。所以联合索引能命中哪些查询取决于最左前缀原则WHERE name 张三命中使用 name 部分。WHERE name 张三 AND age 25命中两个字段都用上。WHERE age 25无法命中联合索引因为跳过了第一个字段 name。WHERE name 张三 AND age 20 AND city 北京name 等值、age 范围能用到但 city 用不到因为 age 的范围条件让 name_age 索引在 age 之后无法继续有序定位。最左前缀原则背后的原因是 BTree 本身就是按字段顺序排序的跳过第一个字段整棵树找不到一个稳定的入口。建联合索引的时候统计上有一个口诀区分度高的字段放前面查询频率均等的时候才考虑等值优先。实际上更准确的做法是经常作为等值查询条件的字段放前面经常做范围查询的字段放后面因为范围查询会让后续字段失效。3.3 覆盖索引少一次回表就多一份性能覆盖索引不是一种独立的索引类型而是一种查询优化的状态查询语句需要的所有列都包含在某个二级索引中。此时 MySQL 不需要回表直接遍历索引就能拿到数据因为二级索引的 BTree 比聚簇索引小一次查询的 I/O 和 CPU 消耗都更低。举个例子CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50), age INT, email VARCHAR(100), INDEX idx_name_age (name, age) );如果执行SELECT name, age FROM user WHERE name 张三;MySQL 在idx_name_age索引里就能直接拿到 name 和 age不需要回表查聚簇索引。但如果是SELECT name, email FROM user WHERE name 张三;email 不在idx_name_age中就需要回表。这也是为什么很多高性能表设计会用“索引冗余字段”的方式来覆盖高频查询把查询涉及的列尽量塞进联合索引让查询变成覆盖索引查询。当然这要权衡写入代价索引字段越多写入越慢空间越大不是无限往里塞。3.4 全文索引与哈希索引适用边界全文索引用在LIKE %keyword%这类模糊匹配场景。InnoDB 的全文索引底层是倒排索引结构支持自然语言检索和布尔检索。要注意 MySQL 5.7 以后 InnoDB 才默认支持中文全文检索能力而且必须使用 ngram 分词插件。建法CREATE FULLTEXT INDEX ft_idx_content ON article (content) WITH PARSER ngram;不过说实话业务里对中文全文搜索我一般更推荐引入 Elasticsearch 这类专用搜索引擎MySQL 全文索引适合轻量级、数据量不大、不想额外维护一套搜索组件的场景。哈希索引在 InnoDB 中默认是不可人工创建的但 InnoDB 内部有一个自适应哈希索引Adaptive Hash Index当某些等值查询被频繁命中且 BTree 访问模式足够规律InnoDB 会自动为这部分热点页建立哈希索引加速等值查找。你没法控制它也不需要控制。MySQL 的 Memory 引擎支持显式创建哈希索引但它不支持范围查询加上 Memory 引擎本身有崩溃丢数据风险生产环境用得很少了解即可。3.5 MySQL 8 中创建索引的标准语法我直接列一下最常用的语法方便你抄-- 普通索引 CREATE INDEX idx_name ON user(name); -- 唯一索引 CREATE UNIQUE INDEX uk_email ON user(email); -- 联合索引 CREATE INDEX idx_name_age ON user(name, age); -- 前缀索引对字段前N个字符建立索引 CREATE INDEX idx_name_prefix ON user(name(10)); -- 全文索引 CREATE FULLTEXT INDEX ft_content ON article(content) WITH PARSER ngram; -- 删除索引 DROP INDEX idx_name ON user;前缀索引在字段超长、比如VARCHAR(500)的 URL 或文本场景很有用能显著减少索引体积代价是选择性可能下降需要提前算一下前缀长度对区分度的影响。4. 使用场景与优缺点什么情况该建什么情况别乱建4.1 各类型索引的场景速查表索引类型最佳场景优点缺点主键索引每张表必须有的聚簇索引查询快数据物理有序随机主键插入会页分裂唯一索引业务唯一字段如手机号保证一致性优化器等值查询写入需唯一校验稍慢普通索引高频等值/范围查询字段灵活成本低无约束能力联合索引多条件组合查询一次索引服务多个查询字段顺序设计不当容易失效前缀索引超长字符串字段省空间减小索引体积可能牺牲区分度不支持覆盖索引优化全文索引轻量级全文检索原生SQL即可实现搜索中文分词弱维护成本高哈希索引等值查询集中场景极速等值查找范围查找无效InnoDB中不可控4.2 索引的隐性代价写入放大与空间成本很多新手只知道“查询慢就建索引”不知道索引是有代价的。每次 INSERT 或 UPDATE 时不仅要更新聚簇索引还要同步维护这张表上的所有二级索引。索引越多写入放大越严重。一个极端例子一张表只有 3 列你却建了 5 个索引每次插入一条数据MySQL 可能要做多次随机 I/O性能反而比没索引还差。所以我的经验是单表二级索引数量控制在 5 个以内除非业务非常明确。新项目上线前用慢查询日志和数据字典分析找出真正被频繁查询的路径再针对性建索引而不是每个字段都先加上。还有一个容易忽视的点索引也占用磁盘和内存缓冲池空间。InnoDB 的缓存池innodb_buffer_pool_size既要缓存数据页也要缓存索引页索引多了能缓存的数据页就少了极端情况下会放大磁盘读。这也是连接池、配置调优时经常被忽略的一环。4.3 为什么有时候明明有索引优化器还是不走索引这是我在实际排查里遇到最多的现象。SQL 里有索引Explain 却显示type ALL或者key NULL。原因基本是这几种查询返回的数据量过大。优化器估算走索引需要大量回表比如你要查表中 30% 以上的数据直接扫聚簇索引可能比走二级索引再回表更快优化器会放弃索引。经典索引失效条件对索引列做函数运算、隐式类型转换、左模糊匹配、OR 条件包含非索引列、联合索引不满足最左前缀等。统计信息不准确。MySQL 的基数统计是采样估算的如果长时间没跑 ANALYZE TABLE统计可能偏差很大导致优化器选错执行计划。基于常见的失效场景我做了一个速查表很多 case 我都在真实服务器上验证过失效场景示例原因函数运算WHERE DATE(created_at) 2024-01-01对索引列做函数处理后无法使用原BTree有序性隐式类型转换WHERE phone 13800138000字符串列与数字比较时MySQL隐式转为数字索引失效前模糊查询WHERE name LIKE %张%BTree必须从最左侧前缀开始定位OR 条件WHERE age 25 OR name 张三OR 两边必须都有索引否则全扫联合索引跳过前导列WHERE age 25索引为 name,age不满足最左前缀原则范围查询右侧列WHERE name 张三 AND age 20 AND score 90索引 name,age,score范围条件阻断后续字段有序查找注意WHERE name 张三 AND age 20 AND score 90其实可以在 MySQL 8.0 的某些场景下利用下推优化继续过滤但那是索引条件下推ICP的机制跟索引能否定位到点是两码事。ICP 能减少回表次数但 score 依然不参与索引的键值定位。5. 实战复盘从慢 SQL 到一个合理索引设计的完整过程5.1 用 EXPLAIN 输出的核心列判断索引执行情况先说怎么验证索引到底走没走对。执行EXPLAIN SELECT ...之后重点看这几列type从好到差依次是 system、const、eq_ref、ref、range、index、ALL。至少要达到 range 或 ref 级别最差是 ALL 全表扫描。key实际选择的索引名NULL 表示没走索引。rows预估扫描行数数字越小通常越好但只是估算。Extra出现Using filesort说明排序没用上索引出现Using temporary说明用了临时表这俩都是性能危险信号。出现Using index则是覆盖索引表现优秀。举例说明我之前优化过一个订单查询接口SELECT order_no, user_id, amount, status FROM orders WHERE user_id 12345 ORDER BY created_at DESC LIMIT 20;原表只有一个主键索引Explain 结果是 typeALL、rows240万、Extra 里有 Using filesort。这个查询是所有用户都能触发的频率极高240 万行扫描加文件排序接口必然慢。5.2 联合索引排序优化减少 filesort 的实操思路遇到排序配合查询条件核心优化思路是让 BTree 的有序性同时满足 WHERE 的定位和 ORDER BY 的排序。上面那个订单查询我建了一个联合索引CREATE INDEX idx_user_created ON orders(user_id, created_at);为什么这么建因为user_id是等值条件created_at是排序字段。索引键先按 user_id 排序再按 created_at 排序所以定位到 user_id12345 的记录时created_at 天然就是按时间排好的MySQL 直接顺序读前 20 条即可不再需要 filesort。再配合把查询改成覆盖索引CREATE INDEX idx_user_created_cover ON orders(user_id, created_at, order_no, amount, status);这样查询需要的所有列都能在二级索引里拿到连回表都省了。实测优化后该查询从 280ms 降到 3ms 左右效果非常明显。这里有一个取舍我用了一个包含 5 个字段的联合索引读写放大是有的但 orders 表常用的查询就是这套字段组合写入频率也不算极高所以性价比很划算。索引设计始终是读写平衡的艺术没有绝对最优只有对具体业务最优。5.3 索引设计前先做这几步排查我给团队定的索引设计流程是这样很枯燥但是很有效打开慢查询日志slow_query_logONlong_query_time1抓一周的真实慢 SQL。把慢 SQL 集合起来去掉重复统计每类 SQL 的执行次数和平均耗时。对每条核心慢 SQL先解释 explain 看执行计划再决定加索引还是改写 SQL。加完索引后在测试环境用生产数据量级验证对比前后执行计划。上线后持续观察慢查询日志谨防统计信息偏差导致执行计划回退。6. 常见问题与排查技巧实录6.1 创建索引很慢甚至锁表怎么办大表加索引时MySQL 8.0 之前 InnoDB 加索引虽然是 online DDL但不同阶段仍可能短暂锁表。我的经验是低峰期执行比如凌晨流量低的时候。先用ALTER TABLE ... ALGORITHMINPLACE, LOCKNONE试着指定允许以可并发读写方式执行。如果表非常大几十亿行那种一次性建索引可能要跑几个小时甚至产生主从延迟。更稳妥的方式是先在备库建索引再切换主从或者使用 gh-ost、pt-online-schema-change 这类在线变更工具能极大减少对线上读写的影响。6.2 为什么删了索引后查询反而变快了遇到过几次。某张报表查询每次要查大量数据比如按天统计汇总走了二级索引后需要回表读取成千上万行还不如直接扫描聚簇索引顺序读。优化器可能在某个时间点选择了索引但数据分布变化后索引路径并不总是最优。这种情况不能只看单条 SQL要结合查询返回的数据量占全表的比例来看。如果比例超过 10% 到 20%直接全表扫描的顺序读取往往更快这是机械硬盘时代就总结出来的经验SSD 时代也大致成立因为顺序 I/O 比随机回表 I/O 永远有优势。6.3 我的几条避坑总结不要在区分度低的字段上建单列索引比如性别、状态。用SELECT COUNT(DISTINCT col) / COUNT(*)算一下区分度太低就别建。不要在高频写的表上无限叠加索引5 个以内差不多是普通业务的安全线。联合索引字段顺序不要凭感觉先算区分度再结合查询条件里是等值还是范围来排。字符串索引能建前缀索引就建前缀索引省的空间远超你想象。每次改完索引后用ANALYZE TABLE更新统计信息别等优化器给你惊喜。我个人在实际排查里最大的体会是索引优化的瓶颈往往不在索引本身而在 SQL 写法。很多人写 SQL 习惯对索引列加函数日期用DATE_FORMAT字符串用CONCAT这些写法再好的索引也发挥不出来。所以我遇到慢查询第一步永远是看 SQL 能不能改写成“对索引友好”的形式第二步才考虑加索引。索引设计没有银弹但只要你理解 BTree 的有序性、回表机制和最左前缀原则80% 的慢查询问题都能靠这几个点直接解决。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →