10.MySQL索引特性
MySQL索引特性
索引的概念
认识磁盘
磁盘的结构
磁盘的随机访问(Random Access)与连续访问(Sequential Access)
MySQL与磁盘交互的基本单位
Buffer Pool
索引的理解
观察主键索引现象
推导主键索引结构的构建
索引结构可以采用哪些数据结构
聚簇索引 VS 非聚簇索引
索引操作
创建主键索引
创建唯一索引
创建普通索引
创建全文索引
查询索引
删除索引
索引创建原则
MySQL索引特性
索引的概念
数据库表里存储的数据都是以记录为单位的,如果直接一条条遍历表中的数据记录进行查询,那时间复杂度就是O(N),这在海量数据场景下简直是灾难。这时候索引的价值就体现出来了——它能让你的查询速度提升成百上千倍,前提是得用对了索引。
索引的本质是数据库底层为表中的数据记录构建的特定数据结构。有了这个结构,查询时就能像查字典一样快速定位目标数据。不过要注意,索引虽然提升了查询速度,但在增删改操作时会拖慢效率。因为每次修改数据,不仅要执行对应操作,还得维护底层的数据结构。
常见的索引类型有四种:
- 主键索引(primary key):唯一且非空,一张表只能有一个
- 唯一索引(unique):确保字段值的唯一性,允许NULL值
- 普通索引(index):最基础的索引类型,没有任何限制
- 全文索引(fulltext):专门用于文本内容的模糊匹配
举个实际案例,假设我们创建了一个包含800万条记录的员工表EMP。在没有索引的情况下,查询某个员工信息平均需要4.5秒。但当我们给工号字段建立索引后,同样的查询几乎检测不到耗时。这就是索引带来的质变——从线性扫描到二分查找的飞跃。
认识磁盘
磁盘的结构
要理解MySQL索引的底层原理,必须先了解磁盘的物理结构。磁盘主要由以下几个关键部件组成:
- 永磁铁:保证磁盘磁性的稳定性
- 音圈马达:读取数据的核心部件
- 主轴:驱动磁盘高速旋转
- 磁头:负责读写数据
- 盘片:存储数据的实际载体
每个盘片的表面被划分为多个同心圆磁道,每个磁道又被分割为若干个扇区。传统扇区大小是512字节,但现代硬盘已普遍采用4KB(4096字节)扇区。
定位扇区采用CHS寻址方式:
- Cylinder(柱面):确定磁道位置
- Head(磁头):确定盘面位置
- Sector(扇区):确定具体扇区
不过操作系统实际使用的是LBA(Logical Block Address)逻辑区块地址来管理磁盘,这实现了硬件层和软件层的解耦。
磁盘的随机访问(Random Access)与连续访问(Sequential Access)
磁盘访问效率的关键在于访问模式:
- 随机访问:访问的扇区地址不连续,需要频繁移动磁头,效率低下
- 连续访问:访问的扇区地址连续,磁头移动少,效率高
举个生活化的例子:就像图书馆找书,如果每次都要跑不同的书架找书(随机访问),肯定比连续在一排书架上找书(连续访问)慢得多。
MySQL与磁盘交互的基本单位
MySQL与磁盘交互的基本单位是16KB的Page(页),这是InnoDB存储引擎的核心数据单元。通过show variables like 'innodb_page_size';
可以看到这个设置。
为什么不是操作系统常用的4KB?这涉及到局部性原理的应用。当读取某条记录时,MySQL会一次性加载整个Page(16KB)到内存。统计显示,下一次访问的数据有较大概率就在同一Page中,这样就减少了磁盘IO次数。
Buffer Pool
MySQL启动时会预先申请一块内存区域叫做Buffer Pool,用来缓存从磁盘加载的数据Page。CRUD操作都发生在Buffer Pool中,之后再以特定策略刷新回磁盘。
这里有个有趣的细节:MySQL和磁盘的交互虽然是以16KB为单位,但实际经过了两层缓冲:
- Buffer Pool → 内核缓冲区:16KB单位
- 内核缓冲区 → 磁盘:4KB单位
这就像快递中转站,先用大卡车(16KB)运输到中转站,再用小货车(4KB)分发到各个网点。
索引的理解
观察主键索引现象
创建一个简单的用户表:
CREATE TABLE user (id INT PRIMARY KEY,age INT,name VARCHAR(20)
);
即使插入数据时故意打乱顺序:
INSERT INTO user VALUES (3,25,'Tom'), (1,30,'Jerry'), (2,28,'Lucy');
查询结果却总是按id有序排列。这是因为MySQL会自动对主键数据进行排序存储,这是构建索引结构的基础。
推导主键索引结构的构建
单个Page
每个Page内部的数据记录以单链表形式组织,但为了加速查询,引入了页内目录。假设某个Page存储了以下数据:
id | name |
---|---|
1 | Alice |
3 | Bob |
5 | Carol |
7 | Dave |
页内目录可能包含这些目录项:
- 1 → 指向id=1的记录
- 5 → 指向id=5的记录
查询id=6时,先找到页内目录中5对应的记录,再往后遍历即可。
多个Page
随着数据量增长,单Page无法容纳所有数据。这时需要多个Page协同工作。每个Page的Page结构体包含:
- 数据区:存储实际记录
- 页内目录:加速单Page内查询
- 系统信息:如Page编号、类型等
页目录的进化
为了加速跨Page查询,引入了页目录:
- 页内目录:管理单个Page内的数据
- 页目录:管理多个Page
- 多级页目录:构建B+树结构
最终形成的就是B+树索引结构:
- 叶子节点存储完整数据记录
- 非叶子节点存储目录项
- 叶子节点之间通过指针连接
索引结构可以采用哪些数据结构
哈希表
优点:O(1)的查询速度
缺点:不支持范围查询,内存消耗大
B树
- 非叶子节点存储数据
- 会导致树的高度增加
- 范围查询效率一般
B+树(MySQL选择)
- 非叶子节点仅存储索引
- 叶子节点存储数据且相互连接
- 支持高效范围查询
- 适合磁盘存储特性
实际存储引擎支持情况
存储引擎 | 支持索引类型 |
---|---|
InnoDB | B+树 |
MyISAM | B+树 |
MEMORY | 哈希、B+树 |
NDB | 哈希、B+树 |
聚簇索引 VS 非聚簇索引
InnoDB的聚簇索引
InnoDB采用聚簇索引,数据直接存储在主键索引的叶子节点中。每个表必须有一个聚簇索引,可以是显式定义的主键,也可以是InnoDB自动生成的隐藏主键。
优势:
- 数据访问更快,聚集索引将数据和索引存储在一起
- 范围查询效率高
缺点:
- 插入顺序影响性能,最好使用自增主键
- 更新主键代价较高
MyISAM的非聚簇索引
MyISAM使用非聚簇索引,数据和索引分开存储。无论是主键索引还是普通索引,叶子节点都存储数据的物理地址。
优势:
- 插入速度更快
- 支持压缩表
缺点:
- 查询需要两次IO:先查索引,再查数据
- 不支持事务
文件结构差异:
- InnoDB:
xxx.frm
(表结构) +xxx.ibd
(数据和索引) - MyISAM:
xxx.frm
(表结构) +xxx.MYD
(数据) +xxx.MYI
(索引)
索引操作
创建主键索引
方式一:建表时指定
CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(20)
);
方式二:建表时最后声明
CREATE TABLE user (id INT,name VARCHAR(20),PRIMARY KEY(id)
);
方式三:建表后添加
ALTER TABLE user ADD PRIMARY KEY(id);
注意:添加主键前要确保字段满足非空且唯一。
创建唯一索引
方式一:建表时指定
CREATE TABLE user (id INT PRIMARY KEY,email VARCHAR(50) UNIQUE
);
方式二:建表时最后声明
CREATE TABLE user (id INT,email VARCHAR(50),PRIMARY KEY(id),UNIQUE(email)
);
方式三:建表后添加
ALTER TABLE user ADD UNIQUE(email);
唯一索引允许NULL值,但只能有一个NULL。
创建普通索引
方式一:建表时声明
CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(20),INDEX idx_name(name)
);
方式二:建表后添加
ALTER TABLE user ADD INDEX idx_name(name);
方式三:使用CREATE语句
CREATE INDEX idx_name ON user(name);
普通索引最灵活,允许重复和NULL值。
创建全文索引
适用于大文本字段的模糊匹配:
CREATE TABLE articles (id INT PRIMARY KEY,title VARCHAR(200),body TEXT,FULLTEXT(title, body)
);-- 查询方式
SELECT * FROM articles WHERE MATCH(title, body) AGAINST('database');
注意:
- MyISAM原生支持全文索引
- InnoDB从MySQL 5.6开始支持
- 最小搜索长度受
ft_min_word_len
参数限制
查询索引
方式一:SHOW KEYS
SHOW KEYS FROM user;
输出字段详解:
- Key_name:索引名称
- Seq_in_index:列在索引中的顺序
- Column_name:索引字段名
- Cardinality:索引基数,越大越好
- Index_type:索引类型(BTREE/FULLTEXT/HASH)
方式二:SHOW INDEX
SHOW INDEX FROM user;
功能与SHOW KEYS相同,输出格式更详细。
方式三:DESC
DESC user;
简单查看索引信息,适合快速浏览。
删除索引
删除主键索引
ALTER TABLE user DROP PRIMARY KEY;
注意:删除主键前要确保没有外键依赖。
删除非主键索引
ALTER TABLE user DROP INDEX idx_name;
-- 或者
DROP INDEX idx_name ON user;
删除索引会影响查询性能,操作前需评估。
索引创建原则
-
高频查询字段优先
经常出现在WHERE、JOIN、ORDER BY中的字段适合创建索引。 -
唯一性要求高的字段
唯一性差的字段(如性别)创建索引效果有限,但组合索引中可能有用。 -
避免频繁更新的字段
更新索引会带来额外开销,如计数器字段不适合做索引。 -
组合索引遵循最左前缀原则
对于idx_name_age(name, age)
索引:WHERE name='Tom'
有效WHERE name='Tom' AND age=25
有效WHERE age=25
无效
-
控制索引数量
每个索引都会占用磁盘空间并影响写入性能,建议单表索引不超过5个。 -
使用前缀索引优化长字段
CREATE INDEX idx_title ON articles(title(20));
对VARCHAR(255)字段只索引前20个字符,节省空间。
-
区分度计算公式
Cardinality / 表记录数 > 0.1 时索引效果较好
举个电商系统的例子:订单表的订单号字段适合创建主键索引,用户ID字段适合创建普通索引,而订单状态字段可能不适合单独创建索引,但可以作为组合索引的一部分。
通过合理使用索引,可以将原本需要扫描百万级数据的查询优化到只需几次磁盘IO,这就是数据库调优的精髓所在。