目录
[什么是 MySQL 索引](#什么是 MySQL 索引)
[基于 B+ 树的 MySQL 索引](#基于 B+ 树的 MySQL 索引)
[聚簇索引 VS 非聚簇索引](#聚簇索引 VS 非聚簇索引)
[主键索引 VS 非主键索引](#主键索引 VS 非主键索引)
什么是 MySQL 索引
- MySQL 的服务器是运行在内存的,所有的数据库的 CURD 操作,也都是在内存中进行的,索引也是如此。
- 提高算法效率的因素:1、组织数据的方式 2、算法本身。举例:对于一个数组,要找一个元素可以采用线性遍历的方式。但如果这个数组是有序的(组织数据的方式),就可以用二分查找(算法本身)提高算法效率。还可以将数组换成哈希结构(组织数据的方式),就可以用哈希算法(算法本身)提高算法效率。
- MySQL 的索引也是同样的道理,它是运行在内存的特定数据结构,以及与它配套的算法,它的作用就是提高 MySQL 的查询效率.
- MySQL 通过操作系统与磁盘交互的基本单位是 16 KB。
索引提高查询效率的直观感受
接下来创建一个有 80 万行的员工信息表,查找这个表的某一行:
查询员工编号为998877的员工
select * from EMP where empno=998877;
1 row in set (4.93 sec)
可以看到耗时4.93秒,这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有 1000个人并发查询,那很可能就死机。
现在给 empno 这一列加上索引
alter table EMP add index(empno);
再查询员工编号为998877的员工
select * from EMP where empno=998877;
1 row in set (0.01 sec)
可以看到只耗时0.01秒
基于 B+ 树的 MySQL 索引
- MySQL 通过操作系统与磁盘交互的基本单位是 16 KB。这个基本数据单元,在 MySQL 这里叫做 page(注意和系统的page区分)
- MySQL 中的数据文件,是以 page 为单位保存在磁盘当中的。
- MySQL 的 CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。 而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。 所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。
- 为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称 Buffer Pool 的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。 为l更高的效率,一定要尽可能的减少系统和磁盘IO的次数
create table if not exists user (
id int primary key,
age int not null,
name varchar(16) not null
);
mysql> show create table user \G
*************************** 1. row ***************************
Table: user
Create Table: CREATE TABLE `user` (
`id` int(11) NOT NULL,
`age` int(11) NOT NULL,
`name` varchar(16) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 --默认就是InnoDB存储引擎
1 row in set (0.00 sec)
mysql> insert into user (id, age, name) values(3, 18, '杨过');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
Query OK, 1 row affected (0.00 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.00 sec)
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
Query OK, 1 row affected (0.00 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:(局部性原理)
为何 MySQL 和磁盘进行 IO 交互的时候,要采用 Page 的方案进行交互呢?用多少,加载多少不香吗? 如上面的 5 条记录,如果 MySQL 要查找 id=2 的记录,第一次加载 id=1,第二次加载 id=2,一次一条记录,那么就需要 2 次 IO。如果要找 id=5,那么就需要 5 次 IO。 但,如果这 5 条(或者更多)都被保存在一个 Page 中(16KB,能保存很多记录),那么第一次 IO 查找 id=2 的时候,整个 Page 会被加载到 MySQL 的 Buffer Pool 中,这里完成了一次 IO。但是往后如果在查找 id=1,3,4,5 等,完全不需要进行 IO 了,而是直接在内存中进行了。所以,就在单 Page 里面,大大减少了 IO 的次数。 你怎么保证,用户一定下次找的数据,就在这个 Page 里面?我们不能严格保证,但是有很大概率,因为有局部性原理。 往往 IO 效率低下的最主要矛盾不是 IO 单次数据量的大小,而是 IO 的次数。
理解单个Page
既然MySQL 通过操作系统与磁盘交互的基本单位是 page,那么一段时间内内存一定存在多个 page,MySQL 要管理好所有的 page,就要先描述 page,再用特定的数据结构组织 page,我们先了解单个 page 的构成:

单个 page 的构成:prev 和 next 构成双向链表(实际肯定更加复杂),里面的数据记录,也构成一个单向链表。
理解多个Page
对书的目录的理解:如果要看书的某一个章节,可以从第一页逐页翻到目标章节,也可以查看目录,然后一下就翻到目标章节,显然第二种方式更快。书中的目录,是多花了纸张的,但是却提高了效率,所以,目录,是一种"空间换时间的做法"。
单页情况
针对上面的单页Page,我们能否也引入目录呢?当然可以

要查找 id=4 的记录,之前必须线性遍历4次, 才能拿到结果。现在直接通过目录23,直接进行定位新的起始位置,现在只需要线性遍历两次,提高了效率。通过目录快速定位的前提是 id 必须是有序的,如同书的页码是有序的,这也解释了上面为什么具有主键约束的表中,乱序插入数据,数据会自动排序 ,其实是为构建目录做准备。
多页情况
既然单页内部可以构建目录,那么页与页之间也可以构建目录。

- 使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值。
- 和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
- 其中,每个目录项的构成是:键值+指针。图中没有画全。
如果页目录仍然有很多,我们还可以给页目录加目录,我称之为"目录的目录"

以上构建目录的过程,就叫作构建主键索引。上面所示的数据结构,与 B+ 树十分相像,它是大多数存储引擎所采用的常见存储结构。这种存储结构提高搜索效率的原因显而易见:查找的本质是排除,这种存储结构除了"叶子结点"是有效数据,其他都是"目录结点"。
- 从算法的角度:通过目录查找,每次都排除一大批数据,提高了效率
- 从 IO 的角度:找到目的行所在的 page ,途径更少的目录 page,减少了 IO 次数,提高了效率
上图就称为 MySQL innodb 下的索引结构,并且对所有表的 CURD,都是对上面的结构进行 CURD。
问题:如果在建表的时候,没有指明那一列是主键,那么这张表的也是像上面一样存储吗?答案是也是像上面一样存储的,如果没有指明那一列是主键,MySQL 会对该表添加一个隐藏列作为主键
InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?
- 链表?肯定不行,它是暴力线性遍历
- 二叉搜索树或者AVL && 红黑树?还是没有 B+ 树优秀,一是二叉搜索树的退化问题,可能退化成为线性结构,二是它们的树高都比 B+ 树更高,这意味着在查找的时候 IO 次数的增多。
- Hash?官方的索引实现方式中, MySQL 是支持 HASH 的,不过 InnoDB 和 MyISAM 并不支持.Hash的算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行,另外还有其他差别,有兴趣可以查一下。
- B 树?B 树的非叶子结点也要存储有效数据,那么非叶子结点存储的目录就变少了, 树的高度就比 B+ 树高,这意味着在查找的时候 IO 次数的增多。并且 B+ 树的叶子节点全部相连,而 B 没有
聚簇索引 VS 非聚簇索引
MyISAM 存储引擎同样使用 B+ 树作为索引结果,与 innodb 不同的是,叶节点的 data 域存放的是数据记录的地址这种存储方式称为非聚簇索引 ,而像 innodb 那样叶节点的 data 域存放的是有效数据的存储方式称为聚簇索引。下图为MyISAM 存储引擎创建的表的主索引, Col1 为主键。

从 Linux 文件系统角度的区别:用 MyISAM 存储引擎创建一张表,会创建三个文件,分别存储
- 表结构数据、
- 该表的有效数据、
- 该表的主键索引数据。
而用 innodb 存储引擎创建一张表,会创建两个文件,分别存储
- 表结构数据、
- 表的有效数据以及主键索引数据。
主键索引 VS 非主键索引
主键索引(被指定为 primary key 的列创建的索引)和非主键索引(没有指定为 primary key 的列创建的索引,如 unique 索引和普通索引),在不同的存储引擎下,查询数据的方式有所不同。
- 在 innodb 存储引擎下:叶子节点存储的是索引列的值 + 对应的主键值 。查询时,先通过普通索引找到主键,再拿着主键去主键索引中查完整数据(这个过程叫回表)。为何I nnoDB 针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?原因就是太浪费空间了。
- 在 MyISAM 存储引擎下:叶子节点存储的是索引列的值 + 数据行的物理地址(指针),与主键索引的结构没有本质区别
索引操作
创建索引
创建索引的建议
适合创建索引的列
1. 频繁出现在
WHERE条件中的列这是最直接的判断标准。查询条件中用得越多的列,越值得建索引。
-- 如果这类查询很多,user_id 就非常适合建索引 SELECT * FROM orders WHERE user_id = 123;2. 频繁用于
JOIN连接的列连接条件(
ON子句)中的列,如果没有索引,MySQL 需要对被连接表做全表扫描,性能极差。
-- orders.user_id 和 users.id 都应该有索引 SELECT * FROM orders JOIN users ON orders.user_id = users.id;3. 频繁用于
ORDER BY或GROUP BY的列索引本身是有序的,可以直接利用索引顺序来排序,避免
filesort(文件排序,性能很差)。
-- 如果经常按 create_time 排序,这个列就适合建索引 SELECT * FROM logs ORDER BY create_time DESC;4. 区分度(选择性)高的列
区分度 =
COUNT(DISTINCT 列名) / COUNT(*),值越接近 1,索引效果越好。
列 区分度 是否适合建索引 id(唯一)1.0 非常适合 手机号接近 1.0 非常适合 性别(男/女)约 0.5 不适合 状态(只有 3-5 种值)很低 通常不适合 为什么区分度低的列不适合?
比如在
性别上建索引,查询WHERE gender = '男'会返回约 50% 的数据,MySQL 优化器会认为全表扫描比用索引更划算(因为用索引还要回表,随机 I/O 太多),最终根本不会走这个索引。5. 字符串列的前缀(当字符串很长时)
如果要对长字符串(如
VARCHAR(200))建索引,可以使用前缀索引,只取前 N 个字符。
CREATE INDEX idx_address ON users (address(20)); -- 只取前20个字符这样既能加速查询,又能大幅节省存储空间和内存。
不适合创建索引的列
1. 很少出现在查询条件中的列
索引是"以写换读"的优化手段。如果一个列几乎不被查询,建索引只会拖慢写入速度,浪费磁盘空间,完全没有收益。
2. 区分度极低的列
如上所述,
性别、是否删除(0/1)、类型(少数几种枚举)这类列,建索引基本没用。3. 频繁更新的列
每次更新该列的值,对应的索引树都要同步更新(删除旧键值 + 插入新键值),维护成本很高。如果查询中确实需要用到,建议权衡利弊。
4. 大文本或二进制列(
TEXT、BLOB)这些列通常存储大量数据,直接建索引会导致索引非常庞大,且查询时无法有效利用索引(除非使用全文索引
FULLTEXT或指定前缀长度)。5. 表的数据量很小
如果一张表只有几百行数据,全表扫描和走索引的代价几乎没差别,甚至全表扫描更快(因为走索引要多一次回表)。这种情况下建索引是"杀鸡用牛刀"。
创建主键索引
第一种方式
-- 在创建表的时候,直接在字段名后指定primary key create table user1( id int primary key, name varchar(30) );第二种方式:
-- 在创建表的最后,指定某列或某几列为主键索引 create table user2( id int, name varchar(30), primary key(id) );第三种方式:
create table user3( id int, name varchar(30) ); -- 创建表以后再添加主键 alter table user3 add primary key(id);
主键索引的特点:
- 指定为 primary key 的列,会自动创建主键索引,这是一个强制且自动的行为,不需要你额外执行任何
CREATE INDEX语句- 一个表中,最多有一个主键索引,当然可以使复合主键
- 主键索引的效率高(主键不可重复)
- 创建主键索引的列,它的值不能为 null,且不能重复
- 主键索引的列基本上是 int
创建唯一键索引
第一种方式
-- 在表定义时,在某列后直接指定 unique 唯一属性。 create table user4( id int primary key, name varchar(30) unique );第二种方式
-- 创建表时,在表的后面指定某列或某几列为 unique create table user5( id int primary key, name varchar(30), unique(name) );第三种方式
create table user6( id int primary key, name varchar(30) ); -- 创建表以后再添加唯一键 alter table user6 add unique(name);
唯一索引的特点:
- 指定为 unique 的列,会自动创建主键索引,这是一个强制且自动的行为,不需要你额外执行任何
CREATE INDEX语句- 一个表中,可以有多个唯一索引
- 查询效率高
- 如果在某一列建立唯一索引,必须保证这列不能有重复数据
- 如果一个唯一索引上指定 not null,等价于主键索引
创建普通索引
第一种方式
create table user8( id int primary key, name varchar(20), email varchar(30), index(name) --在表的定义最后,指定某列为索引 );第二种方式
create table user9(id int primary key, name varchar(20), email varchar(30)); alter table user9 add index(name); --创建完表以后指定某列为普通索引第三种方式
create table user10(id int primary key, name varchar(20), email varchar(30)); -- 创建一个索引名为 idx_name 的索引 create index idx_name on user10(name);
普通索引的特点:
- 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
- 如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引
创建联合索引
第一种方式
create table user8( id int primary key, name varchar(20), email varchar(30), index(name,email) --在表的定义最后,指定某两列或多列为索引 );第二种方式
create table user9( id int primary key, name varchar(20), email varchar(30) ); alter table user9 add index(name,email); --创建完表以后指定某两列或多列为联合索引第三种方式
create table user10( id int primary key, name varchar(20), email varchar(30) ); -- 创建一个索引名为 idx_name 的联合索引 create index idx_name on user10(name,email);
联合索引的特点:
联合索引在B+树中,不是把
name和(name, email)组合成一个整体键值。排序规则是严格的"先主后次":首先按照name排序当name相同时,再按照最左前缀原则 :查询条件必须包含联合索引的"最左前列",索引才会生效。比如:
WHERE name = '张三'(只用第一列) 索引生效:WHERE name = '张三' AND email = 'zhang@a.com'(用全两列) 索引生效,WHERE email = 'zhang@a.com'(跳过了最左列name,索引不会使用) 索引失效索引覆盖 的优势:因为联合索引的叶子节点存储的是
(name, email, id)(主键值会被自动追加),所以如果你只查询这三个字段,不需要回表,直接从索引中就能拿到所有数据,速度非常快。比如:
SELECT id, name, email FROM user8 WHERE name = '张三';直接返回所有'张三'的email
创建前缀索引
主要针对 字符串列 (VARCHAR、CHAR、TEXT、BLOB),且满足以下条件时:
该列长度较长(比如存的是地址、文章摘要、日志信息)。
该列经常出现在
WHERE条件中,需要加速查询。前缀的区分度足够高:取前几个字符就能基本区分出不同的行。前缀长度并不是越长越好,也不是越短越好。判断标准是:在保证足够区分度的前提下,前缀越短越好。
方式1;
-- 创建索引时指定前缀长度
CREATE INDEX idx_name ON 表名 (列名(前缀长度));
方式2;
-- 建表时指定
CREATE TABLE users (
id INT PRIMARY KEY,
address VARCHAR(200),
INDEX idx_address (address(20)) -- 只取前20个字符
);
案例:
-- 假设 users 表有 1000 万行,address 列平均长度 50 个字符
-- 如果对整个 address 列建索引,索引会非常庞大
CREATE INDEX idx_address ON users (address(10)); -- 只取前 10 个字符
因为地址的前 10 个字符(如"北京市海淀区"、"上海市浦东新")已经能过滤掉大部分数据,索引大小却缩小了 5 倍。
创建全文索引
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是有 要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文。如果对中文进 行全文检索,可以使用sphinx的中文版(coreseek)。
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body)
)engine=MyISAM;
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 ...');
如果使用如下查询方式,虽然查询出数据,但是没有使用到全文索引
mysql> select * from articles where body like '%database%';
+----+-------------------+------------------------------------------+
| id | title | body |
+----+-------------------+------------------------------------------+
| 1 | MySQL Tutorial | DBMS stands for DataBase ... |
| 5 | MySQL vs. YourSQL | In the following database comparison ... |
+----+-------------------+------------------------------------------+
// 可以用explain工具看一下,是否使用到索引
mysql> explain select * from articles where body like '%database%'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE <= 简单查询,即没有使用子查询/多表查询等等
table: articles <= 在那张表查询
type: ALL <= 使用全盘查询
possible_keys: NULL
key: NULL <== key为null表示没有用到索引
key_len: NULL
ref: NULL
rows: 6
Extra: Using where
1 row in set (0.00 sec)
如何使用全文索引呢?
mysql> SELECT * FROM articles-> WHERE MATCH (title,body) AGAINST ('database');
+----+-------------------+------------------------------------------+
| id | title | body |
+----+-------------------+------------------------------------------+
| 5 | MySQL vs. YourSQL | In the following database comparison ... |
| 1 | MySQL Tutorial | DBMS stands for DataBase ... |
+----+-------------------+------------------------------------------+
// 通过explain来分析这个sql语句
mysql> explain SELECT * FROM articles WHERE MATCH (title,body) AGAINST
('database')\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE <= 简单查询,即没有使用子查询/多表查询等等
table: articles <= 在那张表查询
type: fulltext <= 使用全文索引
possible_keys: title
key: title <= key用到了 title(全文索引的索引名)
key_len: 0
ref:
rows: 1
Extra: Using where
1 row in set (0.00 sec)
查看索引
语法:
show index from 表名; // index 可以换成 keys //或者 desc 表名; // 信息比较简略示例:
mysql> show index from goods\G *********** 1. row *********** Table: goods <= 表名 Non_unique: 0 <= 0表示唯一索引 Key_name: PRIMARY <= 主键索引 Seq_in_index: 1 Column_name: goods_id <= 索引在哪列 Collation: A Cardinality: 0 Sub_part: NULL Packed: NULL Null: Index_type: BTREE <= 基于B+树的索引 Comment: 1 row in set (0.00 sec)
删除索引
删除主键索引:
alter table 表名 drop primary key;其他索引(唯一键索引或者普通索引)的删除:
alter table 表名 drop index 索引名; // 索引名就是 show index from 表名 中的 Key_name 字段 // 或者 drop index 索引名 on 表名;