一、什么是索引
索引是一种帮助数据库快速查找数据的数据结构,本质是用空间来换时间(额外占用存储,增加写入成本,换来查询时不用逐行扫描整表)类似书前面的目录
常见的索引有:
- 主键索引
- 唯一键索引
- 普通索引
- 全文索引
-- ============================================================
-- 一键脚本:建库、建函数、建表、建存储过程、造测试数据
-- 用法:mysql -u root -p < this_file.sql
-- ============================================================
drop database if exists `bit_index`;
create database if not exists `bit_index` default character set utf8mb4;
use `bit_index`;
-- 避免重复执行时报函数已存在
drop function if exists rand_string;
drop function if exists rand_num;
drop procedure if exists insert_emp;
-- 1. 产生随机字符串(6 位左右)
delimiter $$
create function rand_string(n INT)
returns varchar(255)
deterministic
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str = concat(return_str,
substring(chars_str, floor(1 + rand() * 52), 1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;
-- 2. 产生随机部门号(10 ~ 509,可按需调整)
delimiter $$
create function rand_num()
returns int
deterministic
begin
declare i int default 0;
set i = floor(10 + rand() * 500);
return i;
end $$
delimiter ;
-- 3. 雇员表 EMP(先建表,再建存储过程)
CREATE TABLE `EMP` (
`empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',
`ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',
`job` varchar(9) DEFAULT NULL COMMENT '雇员职位',
`mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',
`hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',
`sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',
`comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',
`deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号',
PRIMARY KEY (`empno`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='雇员表';
-- 4. 存储过程:批量插入 EMP 数据
-- start 起始编号偏移
-- max_num 插入条数
delimiter $$
create procedure insert_emp(in start int, in max_num int)
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)
values (
start + i,
rand_string(6),
'SALESMAN',
0001,
curdate(),
2000,
400,
rand_num()
);
until i = max_num
end repeat;
commit;
set autocommit = 1;
end $$
delimiter ;
-- 5. 造数据
CALL insert_emp(100000, 5000000);
我这里创建了5,000,000个数据的表
(1)查询编号为356677的员工
mysql> select * from EMP where empno=356677;
+--------+--------+----------+------+---------------------+---------+--------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+--------+--------+----------+------+---------------------+---------+--------+--------+
| 356677 | HNQTUR | SALESMAN | 0001 | 2026-10-24 00:00:00 | 2000.00 | 400.00 | 252 |
+--------+--------+----------+------+---------------------+---------+--------+--------+
1 row in set (1.95 sec)
可以看到花费了1.95s,这还是一个人操作,如果多人操作很容易导致死机
解决方法,创建索引
mysql> alter table EMP add index(empno);
Query OK, 0 rows affected (16.72 sec)
Records: 0 Duplicates: 0 Warnings: 0
然后再测试查询时间
mysql> select * from EMP where empno=356617;
+--------+--------+----------+------+---------------------+---------+--------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+--------+--------+----------+------+---------------------+---------+--------+--------+
| 356617 | tylHEY | SALESMAN | 0001 | 2026-10-24 00:00:00 | 2000.00 | 400.00 | 67 |
+--------+--------+----------+------+---------------------+---------+--------+--------+
1 row in set (0.00 sec)
二、MySQL与磁盘交互
Mysql作为一款应用软件,可以想象成一种特殊的文件系统、有着更高的io场景,所以为了提高基本的IO效率,MySQL进行IO的基本单位是16kb
mysql> SHOW GLOBAL STATUS LIKE 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.01 sec)
16*1024=16384
也就是说,磁盘的这个硬件设备的基本单位是512字节,而MySQL InnoDB引擎使用16kb进行IO交互。即MySQL和硬盘进行数据交互的基本单位是16kb。这个基本数据单元,在MySQL里叫Page(和系统的Page不同)
MySQL中的数据文件都是以page为单位保存在磁盘当中的
MySQL的CURD操作,都需要计算来找到对应的位置
由于涉及到计算,就需要CPU参与,而为了CPU参与,一定要能够先将数据移动到内存当中。
所以在特定时间里,数据一定是磁盘中有,而内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘,这就涉及到磁盘和内存的数据交互了,也就是IO,而此时的IO的基本单位就是Page。
为了更好的进行上面的操作,MySQL服务器在内存中运行的时候,就申请了内存空间来进行缓存
为了更高的效率,一定要尽可能的减少系统和磁盘IO的次数
三、索引
建立测试表
mysql> create table user(
-> id int primary key,
-> age int not null,
-> name varchar(16) not null
-> );
Query OK, 0 rows affected (0.08 sec)
mysql> show create table user \G
*************************** 1. row ***************************
Table: user
Create Table: CREATE TABLE `user` (
`id` int NOT NULL,
`age` int NOT NULL,
`name` varchar(16) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
插入
mysql> insert into user (id, age, name) values(3, 18, '杨过');
Query OK, 1 row affected (0.02 sec)
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
Query OK, 1 row affected (0.02 sec)
mysql> insert into user (id, age, name) values(2, 26, '黄蓉');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(5, 36, '郭靖');
Query OK, 1 row affected (0.02 sec)
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
Query OK, 1 row affected (0.02 sec)
查看插入数据 发现是有序
mysql> select * from user;
+----+-----+-----------+
| id | age | name |
+----+-----+-----------+
| 1 | 56 | 欧阳锋 |
| 2 | 26 | 黄蓉 |
| 3 | 18 | 杨过 |
| 4 | 16 | 小龙女 |
| 5 | 36 | 郭靖 |
+----+-----+-----------+
5 rows in set (0.00 sec)
中断一下---为何IO交互要是Page
为了提高缓存命中率
理解单个Page
MySQL中要管理很多数据表文件,而要管理好这些文件,就需要先描述在组织,我们可以理解为一个个独立文件是有一个或多个Page构成的

不同的Page,在MySQL中,都是16Kb,使用prev和next构造双向链表
因为有主键的问题,MySQL会默认根据主键来给我们的数据进行排序
为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?
插入数据时排序的目的,就是优化查询的效率。
页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询
的效率是必须的。
正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是
可以提前结束查找过程的。
理解多个Page
通过上面分析,我知道了,上面页模式中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。
如果有很多数据,一定要用多个Page来保存数据,多个Page彼此使用双链表连接起来,而且每个Page内部的数据也是基于链表的,那么查找特定一条记录,也一定是线性查找。

单页情况

在一个page内部,如果我们要查找id=4,之前必须进行遍历,但是现在直接通过目录23,进行定位新的起始数据,提高了效率。
多页情况
MySQL中每一页大小只有16kb,单个Page大小固定,随着数据量不断增大,16kb不可能存下所有数据,那么必定会有多个页来存储数据

在单表数据不断被插入的情况下,MySQL会在容量不足的时候,自动开辟新的数据,然后通过指针的方式,组织起来所有的Page。
由于page遍历也会很多,因此我们会给page也带上目录

其实目录页本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
所以目录页也可被管理起来

这就类似于B+树,现在再进行寻找特定id的人,就会发现查找次数大幅减少,IO次数减少,效率提高。
B+vsB
B树

B+树

- B树结点,既有数据,又有Page指针,而B+,只有叶子结点有数据,其他目录页,只有键值和Page结点
- B+叶子结点,全部相连,而B没有
为什么选择B+
- 节点不存储data,这样一个节点就可以存储更多key。可以使得树更矮,所以IO操作次数更少。
- 叶子结点相连,更便于进行范围查找
聚簇索引VS非聚簇索引
非聚簇索引
MyISAM存储引擎-主键索引
MyISAM引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址

其中,MyISAM最大的特点就是,将索引page和数据的page分离,也就是叶子结点没有数据,只有对应的地址
聚簇索引
相较于Innodb索引,Innodb是将索引和数据放在一起的。
InnoDB索引这种用户数据和索引数据分离的索引方案,叫做聚簇索引
当然,mysql除了会默认建立主键索引,还会根据用户的其他列信息建立索引,一般这种索引叫普通索引。
对于MyISAM,建立普通索引和主键索引没有什么差别,无非就是主键不能重复,而非主键可以重复。
下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别

普通索引
同时,InnoDB除了主键索引,用户也会建立普通索引

普通索引,找到目标记录,需要两遍索引,先通过辅助索引获得主键,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询。
四、索引操作
1.创建主键索引
(1)在创建表的时候,直接在字段后面加primary key
create table user1(id int primary key, name varchar(30));
(2)在创建表的最后,指定某列或某几列为主键索引
create table user2(id int, name varchar(30), primary key(id));
(3)创建表后在加主键
create table user3(id int, name varchar(30));
-- 创建表以后再添加主键
alter table user3 add primary key(id);
主键索引的特点:
- 一个表中,最多有一个主键索引,当然可以使用复合主键
- 主键索引的效率高(不能重复)
- 创建主键索引的列,它的值不能为null,且不能重复
- 主键索引的列基本上是int
2.唯一索引的创建
(1)在表定义的时候,后面跟unique
create table user4(id int primary key, name varchar(30) unique);
(2)创建表的时候,在表后面指定某列或几列
create table user5(id int primary key, name varchar(30), unique(name));
(3)创建表后进行添加
create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);
唯一索引的特点:
- 一个表中,可以有多个唯一索引
- 查询效率高
- 如果在某一列建立唯一索引,必须保证这列不能有重复数据
- 如果一个唯一索引上指定not null,等价于主键索引
3.普通索引的创建
(1)在表的定义最后
create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name) --在表的定义最后,指定某列为索引
);
(2)创建完后指定某列为普通索引
create table user9(id int primary key, name varchar(20), email
varchar(30));
alter table user9 add index(name);
(3)创建一个索引为 idx_name 的索引
create table user10(id int primary key, name varchar(20), email
varchar(30));
-- 创建一个索引名为 idx_name 的索引
create index idx_name on user10(name);
普通索引的特点:
- 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
- 如果某列需要创建索引,但是该列有重复的值,就可以考虑普通索引
4.全文索引的创建
当对文段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文检索机制。
mysql> CREATE TABLE articles (
NSIGNED AUTO_INCREMENT NOT NULL -> id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
-> title VARCHAR(200),
-> body TEXT,
-> FULLTEXT (title,body)
-> )engine=MyISAM;
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO articles (title,body) VALUES
-> ('MySQL Tutorial','DBMS stands for DataBase ...'),
-> ('How To Use MySQL Well','After you went through a ...'),
-> ('Optimizing MySQL','In this tutorial we will show ...'),
-> ('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
-> ('MySQL vs. YourSQL','In the following database comparison ...'),
-> ('MySQL Security','When configured properly, MySQL ...');
Query OK, 6 rows affected (0.04 sec)
Records: 6 Duplicates: 0 Warnings: 0
mysql> select * from articles where body like '%SQL%';
+----+-------------------+-------------------------------------+
| id | title | body |
+----+-------------------+-------------------------------------+
| 4 | 1001 MySQL Tricks | 1. Never run mysqld as root. 2. ... |
| 6 | MySQL Security | When configured properly, MySQL ... |
+----+-------------------+-------------------------------------+
2 rows in set (0.01 sec)
5.explain
explain用来查看是否用到索引
mysql> explain select * from articles where body like '%SQL%' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: articles
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL <= key 为null 表示没有用到
key_len: NULL
ref: NULL
rows: 6
filtered: 16.67
Extra: Using where
1 row in set, 1 warning (0.01 sec)
使用全文索引
mysql> explain select * from articles where match(title,body) against ('SQL')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: articles
partitions: NULL
type: fulltext
possible_keys: title
key: title
key_len: 0
ref: const
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
6.查询索引
(1)show keys from 表名
mysql> show keys from articles\G
*************************** 1. row ***************************
Table: articles
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 6
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
*************************** 2. row ***************************
Table: articles
Non_unique: 1
Key_name: title
Seq_in_index: 1
Column_name: title
Collation: NULL
Cardinality: NULL
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: FULLTEXT
Comment:
Index_comment:
Visible: YES
Expression: NULL
*************************** 3. row ***************************
Table: articles
Non_unique: 1
Key_name: title
Seq_in_index: 2
Column_name: body
Collation: NULL
Cardinality: NULL
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: FULLTEXT
Comment:
Index_comment:
Visible: YES
Expression: NULL
3 rows in set (0.00 sec)
为什么2和3都是title呢?
这是因为在匹配的过程中,会先进行最左匹配
(2)show index from 表名;
(3)desc 表名
7.删除索引
(1)删除主键索引
mysql> alter table articles drop primary key;
Query OK, 6 rows affected (0.03 sec)
Records: 6 Duplicates: 0 Warnings: 0
(2)删除其他索引
mysql> alter table articles drop index title;
Query OK, 6 rows affected (0.04 sec)
Records: 6 Duplicates: 0 Warnings: 0
mysql> show index from articles;
Empty set (0.01 sec)
(3)drop index 索引名 on 表名
mysql> show index from articles\G
*************************** 1. row ***************************
Table: articles
Non_unique: 1
Key_name: idx
Seq_in_index: 1
Column_name: title
Collation: A
Cardinality: NULL
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
1 row in set (0.00 sec)
mysql> drop index idx on articles;
Query OK, 6 rows affected (0.04 sec)
Records: 6 Duplicates: 0 Warnings: 0
索引创建原则
- 比较频繁作为查询条件的字段应该创建索引
- 唯一性太差的不适合创建
- 更新非常频繁的字段也不适合
- 不会出现在where子句里的字段不应该创建索引