【MySQL】MySQL索引详解:原理、类型与优化策略
一、索引基础概念
(一)什么是索引
索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。索引的作用就相当于书的目录,通过索引可以快速定位到需要的数据,而不需要全表扫描,从而显著提高查询效率。
在MySQL中,索引是存储引擎层面实现的,不同的存储引擎对索引的实现方式可能会有所不同。但无论是哪种存储引擎,索引的目的都是提高数据检索的效率,降低数据库的IO成本。
(二)索引的优缺点
1. 索引的优点
- 提高查询速度:通过索引,数据库可以大幅减少需要扫描的数据量,直接定位到符合条件的记录,从而显著加快数据检索速度,减少磁盘I/O次数。
- 保证数据唯一性:通过创建唯一索引,可以确保表中的某一列(或几列组合)的值是独一无二的,比如用户ID、邮箱等。主键本身就是一种唯一索引。
- 加速排序和分组:如果查询中的ORDER BY或GROUP BY子句涉及的列建有索引,数据库往往可以直接利用索引已经排好序的特性,避免额外的排序操作。
2. 索引的缺点
- 创建和维护耗时:创建索引本身需要时间,特别是对大表操作时。更重要的是,当对表中的数据进行增、删、改(DML操作)时,不仅要操作数据本身,相关的索引也必须动态更新和维护,这会降低这些DML操作的执行效率。
- 占用存储空间:索引本质上也是一种数据结构,需要以物理文件(或内存结构)的形式存储,因此会额外占用一定的磁盘空间。索引越多、越大,占用的空间也就越多。
- 可能被误用或失效:如果索引设计不当,或者查询语句写得不好,数据库优化器可能不会选择使用索引(或者选错索引),反而导致性能下降。
(三)索引适用场景
索引并非在所有场景下都能提高查询性能,以下是一些适合使用索引的场景:
- 数据量大的表:当表中的数据量较大时,全表扫描的成本很高,此时使用索引可以显著提高查询效率。
- 经常需要排序或分组的列:如果某些列经常出现在ORDER BY或GROUP BY子句中,为这些列建立索引可以避免额外的排序操作。
- 经常在WHERE子句中出现的列:为经常用于查询条件的列建立索引,可以加速查询。
- 经常需要连接的列:如果某些列经常用于表连接操作,为这些列建立索引可以提高连接操作的效率。
以下场景则不适合使用索引:
- 数据量小的表:如果表里的数据非常少(比如就几百条),全表扫描可能比通过索引查找更快,因为走索引本身也有开销。
- 查询结果集占比过大:如果要查询的数据占了整张表的大部分(比如超过20%-30%),优化器可能会认为全表扫描更划算。
- 频繁更新的列:如果某列的数据频繁变动,那么索引也需要频繁更新,可能会影响性能。
- 区分度低的列:如性别、状态等只有少数几个不同值的列,索引的效果不明显。
二、索引底层原理
(一)索引数据结构
MySQL中常用的索引数据结构包括B+树、哈希表等,不同的存储引擎可能使用不同的数据结构实现索引。
1. B+树索引
B+树是MySQL中最常用的索引数据结构,特别是在InnoDB和MyISAM存储引擎中。B+树是B树的一种变种,它具有以下特点:
- 所有数据都存储在叶子节点,非叶子节点只存储键值和指针。
- 所有叶子节点之间通过指针连接,形成一个有序链表,方便范围查询。
- B+树的高度一般在2-4层,即使存储大量数据,检索也只需要2-4次磁盘I/O。
- B+树的每个节点可以存储多个键值,这样可以减少树的高度,减少磁盘I/O次数。
相比于其他数据结构,B+树具有以下优势:
- 相比于二叉树,B+树的高度更低,可以减少磁盘I/O次数。
- 相比于B树,B+树的所有数据都存储在叶子节点,非叶子节点只存储键值,可以在相同的磁盘页中存储更多的键值,进一步降低树的高度。
- 叶子节点之间通过指针连接,形成有序链表,方便范围查询和排序操作。
2. 哈希索引
哈希索引基于哈希表实现,通过哈希函数将键值转换为哈希值,然后根据哈希值快速定位到数据。哈希索引具有以下特点:
- 等值查询非常快,时间复杂度接近O(1)。
- 不支持范围查询和排序操作。
- 不支持部分索引列匹配查找。
在MySQL中,Memory存储引擎支持显式创建哈希索引,而InnoDB存储引擎则提供了一种自适应哈希索引(Adaptive Hash Index)的特性,可以在运行时根据访问模式自动构建哈希索引。
(二)索引的物理存储
在MySQL中,索引通常以文件的形式存储在磁盘上。具体的存储方式取决于存储引擎:
- InnoDB:索引和数据存储在同一个文件中(.ibd文件)。
- MyISAM:索引存储在.MYI文件中,数据存储在.MYD文件中。
索引文件通常按照B+树的结构组织,每个节点对应一个磁盘页(通常为16KB),这样可以最大限度地减少磁盘I/O次数。
(三)索引的工作原理
以InnoDB的B+树索引为例,当执行一个查询时,索引的工作原理如下:
- 从B+树的根节点开始,根据键值比较,确定下一步要访问的子节点。
- 沿着B+树逐层向下查找,直到找到叶子节点。
- 在叶子节点中,如果是主键索引(聚集索引),直接返回对应的行数据;如果是辅助索引(非聚集索引),则获取到主键值,再通过主键索引查找对应的行数据(这个过程称为”回表”)。
三、MySQL索引类型
(一)按数据结构分类
1. B+树索引
B+树索引是MySQL中最常用的索引类型,适用于大多数查询场景,特别是范围查询和排序操作。InnoDB和MyISAM存储引擎都使用B+树作为索引结构。
2. 哈希索引
哈希索引主要用于等值查询,不支持范围查询和排序操作。在MySQL中,Memory存储引擎支持显式创建哈希索引,而InnoDB则提供了自适应哈希索引功能。
3. 全文索引
全文索引(FULLTEXT)用于全文搜索,可以在CHAR、VARCHAR和TEXT类型的列上创建。MyISAM和InnoDB(从MySQL 5.6开始)都支持全文索引。
4. 空间索引
空间索引(SPATIAL)用于地理空间数据类型,如POINT、LINESTRING等。MyISAM和InnoDB(从MySQL 5.7开始)都支持空间索引。
(二)按物理存储分类
1. 聚集索引(Clustered Index)
聚集索引决定了表中数据的物理存储顺序。在InnoDB中,表数据按照主键顺序存储,因此主键索引就是聚集索引。聚集索引的叶子节点存储的是整行数据。
特点:
- 一个表只能有一个聚集索引。
- 如果表定义了主键,则主键就是聚集索引。
- 如果表没有定义主键,则使用第一个UNIQUE且NOT NULL的索引作为聚集索引。
- 如果表既没有主键,也没有合适的UNIQUE索引,则InnoDB会自动生成一个隐藏的主键(称为ROW_ID),并使用它作为聚集索引。
2. 非聚集索引(Secondary Index)
非聚集索引也称为辅助索引,它的叶子节点存储的是主键值,而不是整行数据。当使用非聚集索引查询时,如果需要获取索引列以外的数据,则需要通过主键值再次查询聚集索引,这个过程称为”回表”。
特点:
- 一个表可以有多个非聚集索引。
- 非聚集索引的叶子节点存储的是主键值,而不是整行数据。
- 使用非聚集索引查询时,如果需要获取索引列以外的数据,则需要回表。
(三)按逻辑分类
1. 主键索引
主键索引是一种特殊的唯一索引,用于唯一标识表中的每一行数据。主键列不允许有NULL值,一个表只能有一个主键索引。在InnoDB中,主键索引就是聚集索引。
2. 唯一索引
唯一索引(UNIQUE)用于确保索引列的值唯一,但允许有NULL值(NULL不等于NULL)。一个表可以有多个唯一索引。
3. 普通索引
普通索引(INDEX)是最基本的索引类型,没有唯一性限制,允许有重复值和NULL值。
4. 联合索引
联合索引(Composite Index)是在多个列上创建的索引。联合索引的使用遵循”最左前缀原则”,即查询条件必须从索引的最左列开始,不能跳过中间的列。
例如,如果创建了(a, b, c)的联合索引,那么可以使用a、(a,b)、(a,b,c)作为查询条件,但不能直接使用b、c、(b,c)作为查询条件。
四、索引使用策略与优化
(一)索引设计原则
1. 选择合适的列建立索引
- 选择区分度高的列:区分度越高,索引的效果越好。可以通过计算列的基数(Cardinality)与表的总行数的比值来评估区分度。
- 选择经常用于查询条件的列:为经常出现在WHERE子句、JOIN子句、ORDER BY子句和GROUP BY子句中的列建立索引。
- 避免对频繁更新的列建立索引:索引需要维护,如果列的值经常变动,会增加维护成本。
2. 合理使用联合索引
- 遵循最左前缀原则:在创建联合索引时,把最常用的列放在最左边。
- 考虑查询的选择性:在联合索引中,将选择性高的列放在前面,可以更快地缩小查询范围。
- 覆盖索引:尽量使用联合索引覆盖查询所需的所有列,避免回表操作。
3. 控制索引数量
- 避免创建冗余索引:如果已经有(a, b)的联合索引,就不需要再单独创建a的索引。
- 避免创建重复索引:不要在同一列上创建多个索引。
- 定期检查和优化索引:使用SHOW INDEX命令查看索引使用情况,删除不必要的索引。
(二)索引优化技巧
1. 覆盖索引
覆盖索引(Covering Index)是指查询的所有列都包含在索引中,这样就不需要回表查询,可以直接从索引中获取所需的数据。
例如,如果有一个(name, age)的联合索引,那么执行SELECT name, age FROM users WHERE name = 'Tom'
时,就可以直接从索引中获取结果,而不需要回表。
2. 索引下推
索引下推(Index Condition Pushdown, ICP)是MySQL 5.6引入的一种优化技术。在没有ICP之前,存储引擎通过索引检索到数据,然后返回给MySQL服务器,由服务器再判断数据是否符合条件。有了ICP之后,如果存在某些被索引的列的判断条件,MySQL服务器将这些条件”下推”给存储引擎,由存储引擎判断,这样可以减少回表次数和数据传输量。
例如,如果有一个(name, age)的联合索引,执行SELECT * FROM users WHERE name LIKE 'To%' AND age > 20
时,在没有ICP的情况下,存储引擎只会使用name列的索引,然后回表获取完整的行数据,再由MySQL服务器判断age是否大于20。而有了ICP之后,存储引擎会在索引内部判断age是否大于20,只有满足条件的记录才会被回表查询。
3. 前缀索引
对于CHAR、VARCHAR和TEXT等字符串类型的列,如果列的长度很大,可以只索引列值的前缀部分,这样可以减少索引的大小,提高索引的效率。
例如,可以使用CREATE INDEX idx_name ON users(name(10))
只索引name列的前10个字符。但需要注意的是,前缀索引可能会降低索引的选择性,需要根据实际情况权衡。
4. 强制索引
如果MySQL的查询优化器没有选择最优的索引,可以使用FORCE INDEX子句强制使用指定的索引。
例如:SELECT * FROM users FORCE INDEX(idx_name) WHERE name = 'Tom'
。
(三)索引失效场景
以下是一些常见的索引失效场景,了解这些场景可以帮助我们避免索引失效,提高查询效率:
1. 索引列运算
当在WHERE子句中对索引列进行函数运算或表达式计算时,索引会失效。
例如:SELECT * FROM users WHERE YEAR(birth_date) = 1990
会导致索引失效,可以改写为SELECT * FROM users WHERE birth_date BETWEEN '1990-01-01' AND '1990-12-31'
。
2. 隐式类型转换
当索引列的类型与查询条件的类型不一致时,可能会发生隐式类型转换,导致索引失效。
例如,如果phone列是VARCHAR类型,执行SELECT * FROM users WHERE phone = 13800138000
(没有引号)会导致索引失效,应该改为SELECT * FROM users WHERE phone = '13800138000'
。
3. 前导通配符查询
在LIKE查询中使用前导通配符(%或_开头)会导致索引失效。
例如:SELECT * FROM users WHERE name LIKE '%Tom%'
会导致索引失效,而SELECT * FROM users WHERE name LIKE 'Tom%'
则可以使用索引。
4. OR连接条件
当使用OR连接多个条件,且其中有条件的列没有索引时,所有的索引都会失效。
例如:SELECT * FROM users WHERE name = 'Tom' OR age = 20
,如果age列没有索引,则name列的索引也会失效。
5. 最左前缀原则违反
对于联合索引,如果查询条件不符合最左前缀原则,索引会部分或完全失效。
例如,如果有一个(name, age, gender)的联合索引,那么SELECT * FROM users WHERE age = 20 AND gender = 'M'
会导致索引失效,因为没有使用最左边的name列。
6. 范围查询右侧列失效
在联合索引中,如果对某一列进行范围查询(如>、<、BETWEEN等),则该列右侧的所有列都无法使用索引。
例如,如果有一个(name, age, gender)的联合索引,执行SELECT * FROM users WHERE name = 'Tom' AND age > 20 AND gender = 'M'
时,gender列的索引会失效,因为age列进行了范围查询。
(四)执行计划分析
MySQL提供了EXPLAIN命令,可以帮助我们分析SQL语句的执行计划,了解索引的使用情况。
1. EXPLAIN命令基本用法
1 | EXPLAIN SELECT * FROM users WHERE name = 'Tom'; |
EXPLAIN命令会返回一个结果集,包含以下重要字段:
- id:SELECT查询的序号,表示查询中执行SELECT子句或操作表的顺序。
- select_type:SELECT查询的类型。
- table:查询的表名。
- partitions:匹配的分区。
- type:访问类型,表示MySQL如何查找表中的行。常见的类型有system、const、eq_ref、ref、range、index、ALL等,从左到右,性能依次降低。
- possible_keys:可能使用的索引。
- key:实际使用的索引。
- key_len:使用的索引的长度。
- ref:与索引比较的列或常量。
- rows:预计要检查的行数。
- filtered:按表条件过滤的行百分比。
- Extra:额外信息。
2. 常见的访问类型
- system:表只有一行记录,这是const类型的特例。
- const:通过索引一次就找到了,const用于比较主键或唯一索引。
- eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。
- ref:非唯一性索引扫描,返回匹配某个单独值的所有行。
- range:只检索给定范围的行,使用一个索引来选择行。
- index:全索引扫描,遍历整个索引树。
- ALL:全表扫描,从头到尾扫描表中的每一行。
3. 执行计划优化
通过分析执行计划,我们可以发现以下问题并进行优化:
- type为ALL:表示全表扫描,性能较差,应考虑添加适当的索引。
- key为NULL:表示没有使用索引,应检查索引设计或查询条件。
- rows值很大:表示需要检查的行数很多,应考虑优化索引或查询条件。
- Extra中包含Using filesort或Using temporary:表示需要额外的排序或临时表操作,可能影响性能。
五、MySQL索引实践案例
(一)基本索引操作
1. 创建索引
1 | -- 创建表时指定索引 |
2. 查看索引
1 | SHOW INDEX FROM users; |
3. 删除索引
1 | DROP INDEX idx_age ON users; |
(二)常见索引优化案例
1. 优化ORDER BY和GROUP BY
如果ORDER BY或GROUP BY子句中的列有索引,可以避免额外的排序操作。
1 | -- 创建索引 |
2. 优化JOIN操作
在JOIN操作中,如果连接列有索引,可以提高连接效率。
1 | -- 创建索引 |
3. 使用覆盖索引避免回表
通过使用覆盖索引,可以避免回表操作,提高查询效率。
1 | -- 创建索引 |
4. 使用前缀索引优化字符串索引
对于长字符串列,可以使用前缀索引减少索引大小。
1 | -- 创建前缀索引 |
(三)索引维护与监控
1. 定期分析表
使用ANALYZE TABLE命令更新表的统计信息,帮助优化器做出更准确的决策。
1 | ANALYZE TABLE users; |
2. 监控索引使用情况
使用性能模式(Performance Schema)和information_schema来监控索引的使用情况。
1 | -- 查看索引使用情况 |
3. 优化索引结构
使用OPTIMIZE TABLE命令重建表和索引,减少碎片。
1 | OPTIMIZE TABLE users; |
六、总结与最佳实践
(一)索引设计总结
- 选择合适的列:选择区分度高、经常用于查询条件的列建立索引。
- 控制索引数量:避免创建冗余索引和重复索引,定期检查和优化索引。
- 合理使用联合索引:遵循最左前缀原则,考虑查询的选择性。
- 注意索引的维护成本:索引会影响INSERT、UPDATE和DELETE操作的性能。
(二)索引使用最佳实践
- 避免索引失效:了解索引失效的场景,避免在查询中使用会导致索引失效的操作。
- 使用覆盖索引:尽量使用索引覆盖查询所需的所有列,避免回表操作。
- 合理使用索引提示:在必要时使用FORCE INDEX等索引提示。
- 定期维护索引:使用ANALYZE TABLE和OPTIMIZE TABLE命令维护索引。
(三)索引优化技巧总结
- 使用EXPLAIN分析查询:使用EXPLAIN命令分析SQL语句的执行计划,了解索引的使用情况。
- 优化查询语句:根据索引的特性优化查询语句,避免使用会导致索引失效的操作。
- 考虑数据分布:根据数据的分布情况选择合适的索引类型和策略。
- 平衡索引和表的大小:索引会占用额外的存储空间,需要在性能和空间之间做出平衡。
通过合理设计和使用索引,我们可以显著提高MySQL数据库的查询性能,降低系统响应时间,提升用户体验。但同时也要注意索引的维护成本,避免过度索引对系统性能造成负面影响。
参考资料
- MySQL官方文档:https://dev.mysql.com/doc/refman/8.0/en/optimization-indexes.html
- 《高性能MySQL》(第4版)- Baron Schwartz, Peter Zaitsev, Vadim Tkachenko
- 《MySQL技术内幕:InnoDB存储引擎》- 姜承尧
- 《数据库索引设计与优化》- Tapio Lahdenmaki, Michael Leach