MySQL 之索引的结构、分类与语法
一、索引概述
在 MySQL 数据库中,索引就如同书籍的目录,它能够极大地提高数据查询的效率。没有索引时,MySQL 需要对表进行全表扫描来查找目标数据,当表中数据量较大时,这种方式会消耗大量的时间和资源;而有了索引,MySQL 可以通过索引快速定位到数据所在的位置,从而显著减少查询时间。
需要注意的是,索引并非越多越好。虽然索引能加快查询速度,但它也会占用额外的存储空间,并且在进行数据插入、更新和删除操作时,需要同时维护索引,这会降低这些操作的性能。因此,在创建索引时,需要根据实际的业务场景和查询需求进行合理规划。
二、MySQL 索引的结构
(一)B-Tree索引
B-Tree是一种平衡多路查找树,广泛应用于数据库索引结构中。它的每个节点可以存储多个关键字和对应的指针,具有以下显著特点:
平衡结构特性
- B-Tree严格保持左右子树高度差不超过1
- 这种平衡性保证了从根节点到任意叶子节点的路径长度基本相同
- 示例:在一个包含100万条数据的表中,B-Tree索引通常只需3-4次磁盘I/O就能定位到数据
多路分支优势
- 每个节点可以有M个分支(M通常为几百)
- 相比二叉树,显著减少了树的高度
- 实际测试表明,对于相同数据量,B-Tree比二叉树减少60%以上的节点访问次数
关键字有序排列
- 节点内关键字按升序排列:[K₁, K₂, ..., Kₙ]
- 每个关键字Kᵢ对应两个指针:
- Pᵢ指向的子树中所有关键字 < Kᵢ
- Pᵢ₊₁指向的子树中所有关键字 ≥ Kᵢ
- 这种结构支持高效的二分查找算法
注意:虽然理论上存在B-Tree索引,但在MySQL的InnoDB和MyISAM存储引擎中实际使用的是其优化版本B+Tree索引。
(二)B+Tree索引
B+Tree是MySQL最核心的索引结构,InnoDB和MyISAM存储引擎都基于它实现索引,相比B-Tree有以下重要优化:
数据存储优化
- 非叶子节点:仅存储键值和子节点指针(不存实际数据)
- 叶子节点:存储完整数据记录(InnoDB)或数据物理地址(MyISAM)
- 优势:单次查询最多只需访问h个节点(h为树高)
顺序访问特性
- 所有叶子节点通过指针相连形成双向链表
- 范围查询示例:
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31'
索引覆盖查询
实现条件:查询字段都包含在索引中
性能对比测试:
查询类型 平均响应时间(ms) 索引覆盖 2.3 回表查询 8.7 创建复合索引示例:
CREATE INDEX idx_name_age ON users(name, age);
(三)Hash索引
Hash索引基于哈希表实现,主要特点包括:
性能特点
- 等值查询时间复杂度:O(1)
- 测试数据:100万条记录中精确查找仅需0.02ms
使用限制(不支持)
- 部分匹配(LIKE 'abc%')
- 排序操作(ORDER BY)
- 范围查询(>、<、BETWEEN)
哈希冲突处理
- 典型解决方案:链地址法
- 冲突率公式:当负载因子α=n/m > 0.75时性能显著下降 (n=记录数,m=桶数量)
MySQL实现情况
- Memory引擎:默认使用Hash索引
- InnoDB自适应哈希:
- 自动为频繁访问的页创建哈希索引
- 通过参数innodb_adaptive_hash_index控制
(四)Full-Text索引
全文索引专门用于文本搜索,核心特性包括:
适用场景
- 支持的数据类型:CHAR、VARCHAR、TEXT
- 典型应用:文章搜索、产品描述搜索
分词机制
- 英文:按空格和标点分词
- 中文:需要ngram解析器(MySQL 5.7+)
CREATE FULLTEXT INDEX ft_idx ON articles(content) WITH PARSER ngram;
搜索模式对比
模式 语法示例 特点 自然语言模式(默认) AGAINST('数据库')
按相关性排序 布尔模式 AGAINST('+MySQL -Oracle' IN BOOLEAN MODE)
支持+/-等操作符 版本支持情况
- InnoDB支持:MySQL 5.6+
- 性能提示:对于大文本字段,建议单独建表并建立全文索引
三、MySQL 索引的分类
(一)按功能划分
主键索引(Primary Key Index)
主键索引是一种特殊的唯一索引,它用于唯一标识表中的每一条记录,一个表只能有一个主键索引。主键索引的关键字不能为空,并且具有唯一性。在创建表时,可以通过PRIMARY KEY关键字来指定主键索引。
示例:
创建一个用户表并指定 id 字段为主键索引:
CREATE TABLE user (id INT NOT NULL AUTO_INCREMENT,name VARCHAR(50) NOT NULL,age INT,email VARCHAR(100),PRIMARY KEY (id)
);
特点:
- 当插入数据时,如果 id 字段的值重复或为空,MySQL 会报错,从而保证了数据的唯一性和完整性
- 主键索引的查询效率非常高,因为 InnoDB 存储引擎会将表中的数据按照主键索引的顺序进行存储(聚集索引)
- 主键字段通常设置为自增(AUTO_INCREMENT)以确保唯一性
- 主键索引在InnoDB中会作为聚集索引使用
应用场景:
- 用户表的用户ID
- 订单表的订单号
- 任何需要唯一标识记录的场合
唯一索引(Unique Index)
唯一索引用于保证索引字段的值具有唯一性,但允许字段的值为空(一个表中可以有多个字段允许为空)。与主键索引不同,一个表可以有多个唯一索引。通过UNIQUE关键字可以创建唯一索引。
示例:
为用户表的 email 字段创建唯一索引:
CREATE UNIQUE INDEX idx_user_email ON user (email);
特点:
- 创建唯一索引后,如果插入或更新数据时,email 字段的值与已有的值重复,MySQL 会报错
- 唯一索引允许字段的值为空,但可以有多个NULL值(因为NULL不等于任何值,包括NULL)
- 唯一索引可以提高查询效率,同时保证数据唯一性
- 相比普通索引,唯一索引在插入或更新时需要额外检查唯一性约束
应用场景:
- 用户邮箱、手机号等需要唯一但不作为主键的字段
- 避免业务上不允许重复的数据
普通索引(Normal Index)
普通索引是最基本的索引类型,它没有唯一性约束,也没有非空约束,主要用于提高数据查询的效率。通过INDEX或KEY关键字可以创建普通索引。
示例:
为用户表的 name 字段创建普通索引:
CREATE INDEX idx_user_name ON user (name);
特点:
- 可以加快对 name 字段的查询速度
- 执行
SELECT * FROM user WHERE name = '张三'
查询时,MySQL 可以通过 idx_user_name 索引快速定位到相关数据 - 不影响数据的插入和更新速度(不像唯一索引需要检查唯一性)
- 可以创建多个普通索引,但索引过多会影响写入性能
应用场景:
- 经常作为查询条件的字段
- 需要提高查询性能但不需要唯一约束的字段
- JOIN操作中经常使用的字段
全文索引(Full-Text Index)
全文索引主要用于对文本类型的数据进行全文检索,能够高效地进行关键词搜索和大文本字段的模糊匹配。
示例:
创建文章表的 content 字段全文索引:
CREATE FULLTEXT INDEX idx_article_content ON article (content);
特点:
- 适用于MyISAM和InnoDB引擎(MySQL 5.6+)
- 只能用于CHAR、VARCHAR和TEXT类型的字段
- 使用MATCH() AGAINST()语法进行全文搜索
- 支持自然语言搜索和布尔搜索模式
- 对于中文全文检索需要配合分词插件
应用场景:
- 文章内容搜索
- 产品描述搜索
- 任何需要文本内容搜索的场景
(二)按索引字段数量划分
单列索引(Single-Column Index)
单列索引是指只基于表中的一个字段创建的索引,前面提到的主键索引、唯一索引、普通索引和全文索引都可以是单列索引。
示例:
为 user 表的 age 字段创建普通单列索引:
CREATE INDEX idx_user_age ON user (age);
特点:
- 优点:创建和维护简单,查询时只需要根据单个字段进行匹配
- 缺点:当查询条件涉及多个字段时,单列索引的效率可能较低
- 适合在单个字段查询频率高的场景使用
- 可以为同一字段创建不同类型的单列索引(如唯一索引和普通索引不能同时存在)
应用场景:
- 单个字段的等值查询
- 单个字段的范围查询
- 排序操作(SORT BY)中使用的单个字段
复合索引(Composite Index)
复合索引(也称为联合索引)是指基于表中的多个字段创建的索引,它将多个字段组合在一起作为索引关键字。
示例:
创建name和age的复合索引:
CREATE INDEX idx_user_name_age ON user (name, age);
最左前缀原则详解:
在复合索引中,字段的顺序非常重要,MySQL 会按照索引中字段的顺序进行排序和查找。对于 idx_user_name_age 复合索引:
- MySQL 首先会按照 name 字段进行排序
- 对于 name 字段值相同的记录,再按照 age 字段进行排序
可以使用该索引的查询示例:
SELECT * FROM user WHERE name = '张三';
SELECT * FROM user WHERE name = '张三' AND age = 25;
SELECT * FROM user WHERE name LIKE '张%'; -- 模糊查询中%在右侧时可以使用索引
无法使用该索引的查询示例:
SELECT * FROM user WHERE age = 25;
SELECT * FROM user WHERE name LIKE '%三'; -- 模糊查询中%在左侧时无法使用索引
SELECT * FROM user WHERE age = 25 AND name = '张三'; -- 字段顺序与索引顺序不一致
优化建议:
- 将查询频率高的字段放在复合索引的前面
- 选择性高的字段(唯一值多的字段)放在前面
- 遵循最左前缀原则编写查询条件
- 避免创建过多复合索引,一般3-5个字段为宜
应用场景:
- 多条件联合查询
- 多字段排序
- 覆盖索引场景(索引包含所有查询字段)
(三)按存储结构划分
根据索引的存储结构,MySQL 索引可以分为聚集索引(Clustered Index)和非聚集索引(Non-Clustered Index),这种分类方式主要与 InnoDB 存储引擎相关。
聚集索引(Clustered Index)
特点:
- 索引的叶子节点存储的是完整的数据记录
- InnoDB 存储引擎的主键索引就是聚集索引
- 数据是按照聚集索引的顺序进行存储的
- 一个表只能有一个聚集索引
- 查询效率非常高,特别是根据主键查询时
工作机制:
- 当根据主键查询时,可以直接通过聚集索引获取完整数据
- 范围查询效率高,可以快速定位范围的起始和结束位置
- 如果没有显式定义主键,InnoDB会选择:
- 第一个非空的唯一索引作为聚集索引
- 如果没有,则自动创建一个隐藏的6字节主键作为聚集索引
示例:
-- 聚集索引查询(高效)
SELECT * FROM user WHERE id = 10;-- 范围查询(高效)
SELECT * FROM user WHERE id BETWEEN 10 AND 20;
非聚集索引(Non-Clustered Index)
特点:
- 索引的叶子节点存储的是主键值(InnoDB)或物理地址(MyISAM)
- 需要"回表"操作获取完整数据
- 一个表可以有多个非聚集索引
- 查询效率通常低于聚集索引
InnoDB的非聚集索引工作机制:
- 首先在非聚集索引中找到主键值
- 然后到聚集索引中获取完整数据(回表)
MyISAM的非聚集索引特点:
- 所有索引都是非聚集索引
- 叶子节点存储数据的物理地址
- 直接通过物理地址获取数据,不需要回表
示例:
-- 假设name字段有非聚集索引
SELECT * FROM user WHERE name = '张三';
-- 执行过程:
-- 1. 在name索引中查找"张三"对应的主键id
-- 2. 用找到的id到主键索引中获取完整记录
优化建议:
- 尽量使用覆盖索引(索引包含所有查询字段)
- 避免不必要的回表操作
- 对于频繁查询的非主键字段,考虑创建适当的非聚集索引
应用场景:
- 非主键字段的查询
- 需要建立多个索引的场景
- 覆盖索引优化查询性能
四、MySQL 索引的语法
索引类型
MySQL 支持多种索引类型:
- 主键索引(PRIMARY KEY)
- 唯一索引(UNIQUE)
- 普通索引(INDEX/KEY)
- 全文索引(FULLTEXT)
- 复合索引(多列组合索引)
一、创建索引
1. 创建表时创建索引
主键索引
主键索引是特殊的唯一索引,不允许NULL值,且每个表只能有一个主键。
CREATE TABLE user (id INT NOT NULL AUTO_INCREMENT,name VARCHAR(50) NOT NULL,age INT,email VARCHAR(100),PRIMARY KEY (id) -- 主键索引
);
唯一索引
确保索引列的值唯一,允许NULL值(但NULL值可以有多个)。
CREATE TABLE user (id INT NOT NULL AUTO_INCREMENT,name VARCHAR(50) NOT NULL,age INT,email VARCHAR(100) UNIQUE, -- 方式一:列约束形式PRIMARY KEY (id),UNIQUE INDEX idx_user_age (age) -- 方式二:表约束形式
);
普通索引
最基本的索引类型,没有唯一性限制。
CREATE TABLE user (id INT NOT NULL AUTO_INCREMENT,name VARCHAR(50) NOT NULL,age INT,email VARCHAR(100),PRIMARY KEY (id),INDEX idx_user_name (name), -- 普通索引(方式一)KEY idx_user_email (email) -- 普通索引(方式二,KEY与INDEX等价)
);
全文索引
主要用于文本内容的全文搜索,仅支持MyISAM和InnoDB(MySQL 5.6+)存储引擎。
CREATE TABLE article (id INT NOT NULL AUTO_INCREMENT,title VARCHAR(100) NOT NULL,content TEXT,PRIMARY KEY (id),FULLTEXT INDEX idx_article_content (content) -- 全文索引
);
复合索引
对多个列的组合建立索引,遵循最左前缀原则。
CREATE TABLE user (id INT NOT NULL AUTO_INCREMENT,name VARCHAR(50) NOT NULL,age INT,email VARCHAR(100),PRIMARY KEY (id),INDEX idx_user_name_age (name, age) -- 复合索引
);
2. 在已有表上创建索引
使用CREATE INDEX语句
-- 创建普通索引
CREATE INDEX idx_user_name ON user (name);-- 创建唯一索引
CREATE UNIQUE INDEX idx_user_email ON user (email);-- 创建复合索引
CREATE INDEX idx_user_name_age ON user (name, age);-- 创建全文索引
CREATE FULLTEXT INDEX idx_article_content ON article (content);
使用ALTER TABLE语句
-- 创建普通索引
ALTER TABLE user ADD INDEX idx_user_age (age);-- 创建唯一索引
ALTER TABLE user ADD UNIQUE INDEX idx_user_email (email);-- 创建主键索引
ALTER TABLE user ADD PRIMARY KEY (id);-- 创建全文索引
ALTER TABLE article ADD FULLTEXT INDEX idx_article_content (content);-- 创建复合索引
ALTER TABLE user ADD INDEX idx_user_name_age (name, age);
二、查看索引
基本语法
SHOW INDEX FROM 表名 [FROM 数据库名];
-- 或
SHOW KEYS FROM 表名 [FROM 数据库名];
输出字段说明
字段名 | 说明 |
---|---|
Table | 索引所属的表名 |
Non_unique | 是否允许重复值(0=唯一,1=非唯一) |
Key_name | 索引名称(主键为PRIMARY) |
Seq_in_index | 字段在索引中的位置 |
Column_name | 索引字段名 |
Collation | 排序方式(A=升序,NULL=无排序) |
Cardinality | 索引中唯一值的估计数 |
Sub_part | 索引使用的字符数(NULL=全部) |
Packed | 是否压缩存储 |
Null | 是否允许NULL值 |
Index_type | 索引类型(BTREE,HASH,FULLTEXT等) |
Comment | 索引注释信息 |
示例
-- 查看当前数据库中user表的索引
SHOW INDEX FROM user;-- 查看test数据库中user表的索引
SHOW INDEX FROM user FROM test;
三、删除索引
1. 使用DROP INDEX语句
-- 删除普通索引
DROP INDEX idx_user_name ON user;-- 删除唯一索引
DROP INDEX idx_user_email ON user;-- 删除复合索引
DROP INDEX idx_user_name_age ON user;-- 删除全文索引
DROP INDEX idx_article_content ON article;
2. 使用ALTER TABLE语句
-- 删除普通索引
ALTER TABLE user DROP INDEX idx_user_age;-- 删除主键索引
ALTER TABLE user DROP PRIMARY KEY;
注意事项
- 删除索引会释放存储空间,但可能影响查询性能
- 删除主键索引前需确保没有外键约束
- 删除主键后,InnoDB会自动创建隐藏主键(row_id)
四、修改索引
MySQL不支持直接修改索引,需要通过删除重建的方式实现。
修改复合索引字段顺序
-- 1. 删除旧索引
DROP INDEX idx_user_name_age ON user;-- 2. 创建新索引
CREATE INDEX idx_user_age_name ON user (age, name);
修改索引名称
-- 1. 删除旧索引
DROP INDEX idx_user_age ON user;-- 2. 创建新索引
CREATE INDEX idx_user_age_new ON user (age);
索引使用建议
- 为经常用于查询条件的列创建索引
- 为经常用于表连接的列创建索引
- 避免为频繁更新的列创建过多索引
- 考虑使用复合索引替代多个单列索引
- 定期分析和优化索引使用情况
五、索引使用的注意事项与最佳实践
(一)避免过度索引
问题分析
索引虽然能提高查询速度,但会带来额外的维护成本:
- 写入性能下降:每次数据变更(INSERT/UPDATE/DELETE)时,数据库需要同步更新所有相关索引。例如,一个包含10个索引的表,每次写入都需要更新10个索引结构。
- 存储空间增加:索引本身需要占用磁盘空间。一个典型的B+Tree索引通常占用与数据表相当甚至更多的存储空间。
最佳实践建议
高频查询字段优先索引:
- WHERE子句过滤字段(如用户表的
status
字段,用于筛选活跃用户) - JOIN关联字段(如订单表的
product_id
关联商品表) - 排序/分组字段(如日志表的
create_time
用于时间范围查询)
- WHERE子句过滤字段(如用户表的
选择性评估:
- 对区分度高的字段(如用户名、手机号)创建索引效果显著
- 对区分度低的字段(如性别、状态标志)创建索引收益有限
监控工具使用:
-- 查询未使用的索引 SELECT * FROM sys.schema_unused_indexes;
(二)复合索引的最左前缀原则
深入解析
复合索引(A,B,C)
相当于同时创建了三个索引:
(A)
(A,B)
(A,B,C)
实用案例
有效场景:
WHERE A=1 AND B=2
WHERE A=1 AND B>2 AND C=3
WHERE A IN (1,2,3) AND B=4
失效场景:
WHERE B=2
(缺少A字段)WHERE A=1 AND C=3
(跳过B字段)WHERE A LIKE '%test'
(左模糊查询)
设计建议
字段顺序策略:
- 第一顺位:等值查询字段
- 第二顺位:范围查询字段
- 第三顺位:排序字段
实际案例:
-- 用户表优化示例 CREATE INDEX idx_user_region_age ON users(region, age, gender);
(三)索引失效常见场景
详细场景分析
函数操作:
- 错误示例:
WHERE DATE(create_time) = '2023-01-01'
- 优化方案:
WHERE create_time BETWEEN '2023-01-01 00:00:00' AND '2023-01-01 23:59:59'
- 错误示例:
类型不匹配:
- 错误示例:
WHERE id = '100'
(id为INT类型) - 正确示例:
WHERE id = 100
- 错误示例:
OR条件处理:
- 方案一:创建联合索引
(name, email)
- 方案二:改写为UNION查询:
SELECT * FROM users WHERE name = '张三' UNION SELECT * FROM users WHERE email = 'zhangsan@example.com';
- 方案一:创建联合索引
否定查询优化:
- 改写方案:
-- 原查询:WHERE status != 'deleted' -- 优化为: WHERE status IN ('active', 'pending', 'suspended')
- 改写方案:
(四)索引类型选择
类型对比指南
索引类型 | 适用场景 | 优势 | 限制 |
---|---|---|---|
B+Tree | 默认选择 | 支持范围查询、排序 | 占用空间较大 |
Hash | 内存表、精确匹配 | O(1)查询速度 | 不支持范围查询 |
Full-Text | 文本搜索 | 支持自然语言查询 | 仅限文本字段 |
使用示例
Hash索引:
CREATE TABLE cache (key VARCHAR(100) PRIMARY KEY,value TEXT,INDEX USING HASH (key) ) ENGINE=MEMORY;
全文索引:
ALTER TABLE articles ADD FULLTEXT INDEX ft_index (title, content); -- 使用MATCH...AGAINST查询 SELECT * FROM articles WHERE MATCH(title,content) AGAINST('数据库优化' IN NATURAL LANGUAGE MODE);
(五)索引维护策略
完整维护流程
分析阶段:
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id=100 AND status='paid';
优化决策:
- 关键指标检查:
possible_keys
:可能使用的索引key
:实际使用的索引rows
:预估扫描行数
- 关键指标检查:
重建操作:
- 在线重建:
ALTER TABLE orders ALGORITHM=INPLACE, LOCK=NONE, REBUILD;
- 离线重建:
OPTIMIZE TABLE orders;
- 在线重建:
维护周期建议
- 高频率写入表:每周维护
- 中频率表:每月维护
- 低频率表:每季度维护
监控脚本示例
-- 查询索引碎片率
SELECT table_name,index_name,ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS size_mb,stat_description
FROM mysql.innodb_index_stats
WHERE database_name = DATABASE()
AND stat_name = 'size';