索引提高了数据库的性能,特别是提高了海量数据的检索速度。但是查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。
因此索引带来的价值,是提高查找的效率,如果有大量的插入、更新、删除则不建议使用索引。
常见索引分为:
操作系统读取磁盘,是以块为单位的,基本单位是 4KB 。MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB (InnoDB 存储引擎)。
这个基本单位在MySQL 之中叫做Page,即只要发生了数据的交互,哪怕只有1bit,也是需要进行16KB的数据进行交互。
MySQL 在服务器启动的时候,会预先加载一大块空间自己进行内存管理,这块空间被称为Buffer Pool(MySQL 5.7之中为128KB)。
Page的大小是固定的,数据量有限,如果不存储数据,就能够存储更多的索引信息,目录Page能够管理更多的Page,否则目录Page管理的页数太少,整颗树的层数就越多,更深,也就意味着从根节点到叶子节点的Page更多,即需要更多的IO。
以主键(id)索引为例:
由于MySQL和磁盘交互的基本单位是Page,这些Page叫做数据页。我们只需要将数据保存在每个数据页中即可,加载时直接加载一整个数据页,而数据页与数据页之间通过指针连成双向链表,这样就能够获取前一个或后一个数据页。而每条数据之间通过主键进行排序。在每个数据页中也有一个目录,这样在单个数据页中的查询速度就会加快。
在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的数据页来保存新的数据,然后通过指针的方式,将所有的数据页组织起来。但是当数据页多起来的时候,如果加载每个数据页去遍历检测的话,时间会非常慢,这时可以给每个数据页建立起对应的目录,这就是索引。而索引也是通过Page保存的,我们称之为目录页,目录页只放各个下级Page的最小键值:
目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
当顶层的目录页过多时,可以再加一层目录页。
这种组织方式就是B+树。
总结:
上面的组织形式是以主键为索引的组织形式,也就是先按照主键进行排序放到数据页中,再用目录页将数据页组织成B+树。如果没有设置主键该以谁为索引呢?
当我们没有设置主键的时候,InnoDB会优先选取一个唯一键作为索引,如果表中连唯一键也没有的话,就会自动为每一条记录添加一个叫做DB_ROW_ID的列作为默认主键,该列是一个6字节的自增数值,随着插入而自增,但是这个主键我们看不到。
换句话说,为什么主键必须是自增的,用非自增的(比如学号,身份证号)会怎么样?
由于数据页中的记录是按照主键从小到大进行串联的,自增ID决定了后来插入的记录一定会排列在上一条记录的后面,只需要简单添加next_record指针就可以了;如果当前数据页写满,那就放心地直接插入新的数据页中就可以了。
而非自增的主键则不同,它的大小顺序是不确定的,后来插入的记录有可能(而且概率相当大)插入到上一条记录之前(甚至是当前数据页之前),这就意味着需要遍历当前数据页的记录(或者先找到相关的数据页),然后找到自己的位置进行插入;如果当前数据页写满了,只能先找到适合自己位置的数据页,然后在数据页中遍历记录找到自己的合适位置进行插入。
因此使用非自增的主键插入记录花费的时间更长。
有时候我们搜索数据并不是通过主键(或者唯一键)来搜索的,也有可能是通过非主键列来搜索,而主键索引又是通过主键来完成的,所以主键索引就失效了。这时候可以通过建立普通(辅助)索引来解决这一问题。
比如我们想寻找name字段的一条信息,就可以给name字段创建普通索引。普通索引也是一棵B+树,但是和上面主键的B+树有一些区别:
此时通过普通索引,就能够找到name对应的主键了,然后我们就可以再从主键索引中找到name对应的完整信息。这个过程称为回表。
我们的主键也有可能是复合主键。
包含多个列的主键始终会自动以复合索引的形式创建索引,其列的顺序是它们在表定义中出现的顺序,而不是在主键定义中指定的顺序。
假设我们以id列和name列作为主键,那么在系统创建主键索引时,自然也是创建一棵B+树,但就不是以id为索引了,而是以id和name为索引,当id字段相同时,则会按照name排序。并且在搜索时,也是先匹配id字段,然后再name字段。
同理,我们也可以对多个非主键的字段建立普通的复合索引,其方式和上面类似,比如假设我们为name列和phone列建立联合索引,创建一棵B+树:
MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。这种用户数据与索引数据分离的索引方案,叫做非聚簇索引。
InnoDB 的数据和索引是放在一起的,这种用户数据与索引数据在一起索引方案,叫做聚簇索引。
它们之间有如下差别:
创建主键以后,MySQL会自动创建主键索引。
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);
主键索引的特点:
创建唯一键以后,MySQL会自动创建唯一索引。
create table user4(id int primary key, name varchar(30) 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);
唯一索引的特点:
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));
create index idx_name on user10(name);
普通索引的特点:
show keys from 表名 \G;
show index from 表名\G;
desc 表明;
mysql> show keys from t4\G;
*************************** 1. row ***************************(第一个主键)
Table: t4 <- 表名
Non_unique: 0 <- 如果索引不能包括重复值则为0,如果可以则为1。也就是平时所说的唯一索引
Key_name: PRIMARY <- 索引的名字
Seq_in_index: 1 <- 索引中的列序列号,从1开始
Column_name: id <- 索引是那个(索引的列名)
Collation: A <- 列以什么方式存储在索引中,大概意思就是字符序
Cardinality: 0 <- 基数的意思,表示索引中唯一值的数目的估计值
Sub_part: NULL <-前置索引的意思,如果列只是被部分地编入索引,则为被编入索引的字符的数目。如果整列被编入索引,则为NULL
Packed: NULL <-指示关键字如何被压缩。如果没有被压缩,则为NULL
Null: <-如果列含有NULL,则含有YES
Index_type: BTREE <- 以二叉树的形式构建索引
Comment:
Index_comment: <- 注释的意思
mysql> desc t6;
+-------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id | int(11) | YES | MUL | NULL | | (key MUL)表示索引
| name | char(10) | YES | MUL | NULL | |
| grade | int(11) | YES | MUL | NULL | |
+-------+----------+------+-----+---------+-------+
alter table 表名 drop primary key;
alter table 表名 drop index 索引名;
。索引名就是show keys from 表名中的Key_name 字段。比如:
alter table user10 drop index idx_name;
drop index 索引名 on 表名;
比如:
drop index name on user8;
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。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 explain详解
explain 命令获取 select 语句的执行计划,通过 explain 我们可以知道以下信息:表的读取顺序,数据读取操作的类型,哪些索引可以使用,哪些索引实际使用了,表之间的引用,每张表有多少行被优化器查询等信息。
使用下面的脚本创建EMP表并插入八百万行数据
-- 构建一个8000000条记录的数据
-- 构建的海量表数据需要有差异性,所以使用存储过程来创建, 拷贝下面代码就可以了,暂时不用理解
-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
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 ;
-- 产生随机数字
delimiter $$
create function rand_num( )
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
delimiter ;
-- 创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start+i)
,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;
-- 雇员表
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 '部门编号'
);
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
如果不使用主键搜索,使用ename:
由于ename字段并不是索引,所以查找的会很慢
将ename设置为普通索引,此时按照ename的查找速度会大大加快:
可以看到即使主键已经删除了,普通索引还是在的,并且主键删除的时间很长。
这主要是因为主键删除以后,DB_ROW_ID就会成为主键,那Page就会重新排列,并且普通索引叶子节点的主键值也会变为DB_ROW_ID。
索引覆盖
如果一个表中两个字段,比如id和name,两者为复合索引。
如果今天我们指向查找一个id对应的name,那么它在索引的过程中就能找到id和name,不需要到叶子节点,相当于覆盖了后面的获取主键然后回表操作。
索引最左匹配原则
还是id和name为复合索引。
它们在索引过程中是以id排序,id相同才以name排序。此时如果以name查找,就不能用这个复合索引,因为并不是以name排序的。这时候应该以name创建一个普通索引。
索引下推
还是id和name为复合索引。
我们要查询所有id为10,name为’%川’的人的信息,把%加在name字段前面的时候,是无法利用索引的顺序性来进行快速比较的,也就是说这条查询语句中只有id字段可以使用索引进行快速比较和过滤。所以会筛选出所有id为10的主键,然后进行回表操作,如果id为10的信息过多,就会产生多次回表操作。
索引下推就是过滤的动作由下层的存储引擎层通过使用索引来完成,而减少不必要的回表操作。
推荐阅读:
图解|12张图解释MySQL主键查询为什么这么快
图解|这次,彻底理解MySQL的索引