【MySQL】索引及其B+树


需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。


 目录

一、索引初识和测试数据的构建

二、磁盘

三、MySQL、OS、磁盘的交互方式(InnoDB 存储引擎)

四、MySQL中索引和page的理解

1、为什么MySQL和磁盘进行IO交互的时候,要采用page的方案进行交互,而不是采用用多少,加载多少的方式?

2、MySQL对page的管理方式

2.1先来看个错误page的数据结构

2.2单个page内部的正确的数据结构

2.3多个page之间的正确的数据结构

2.4B+树的特征 

2.5MySQL的主键

2.6为什么B+树做索引优于其他数据结构?

2.7聚簇索引和非聚簇索引

2.7.1MyISAM的辅助(普通)索引

2.7.2innodb的辅助(普通)索引

五、索引的操作

1、创建索引

1.1创建主键索引

1.2创建唯一索引(属于普通索引)

1.3创建普通索引/复合索引

1.4创建全文索引

2、查询索引

3、删除索引

4、什么字段应该创建索引 


一、索引初识和测试数据的构建

索引:提高数据库的查找性能。查询速度的提高是以插入、更新、删除的速度为代价,这些写操作,增加了大量的IO。所以它的价值,在于提高一个海量数据的检索速度。

常见索引分为:

主键索引(primary key)

唯一索引(unique)

普通索引(index)

全文索引(fulltext)--解决中子文索引问题。

构建一个8000000条记录的数据:

mysql> source /home/jly/index_data.sql;
Query OK, 0 rows affected (32 min 14.69 sec)
--看一下前5条数据
mysql> select* from EMP limit 5;
+--------+--------+----------+------+---------------------+---------+--------+--------+
| empno  | ename  | job      | mgr  | hiredate            | sal     | comm   | deptno |
+--------+--------+----------+------+---------------------+---------+--------+--------+
| 100002 | YPdZKD | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    377 |
| 100003 | YJmqTw | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    288 |
| 100004 | yIUxHR | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    127 |
| 100005 | JIrHnr | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    455 |
| 100006 | xFJFYc | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    185 |
+--------+--------+----------+------+---------------------+---------+--------+--------+
5 rows in set (0.03 sec)

查找员工编号为998877的员工信息,花费25秒:

mysql> select * from EMP where empno=998877;
+--------+--------+----------+------+---------------------+---------+--------+--------+
| empno  | ename  | job      | mgr  | hiredate            | sal     | comm   | deptno |
+--------+--------+----------+------+---------------------+---------+--------+--------+
| 998877 | HJxoaj | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    463 |
+--------+--------+----------+------+---------------------+---------+--------+--------+
1 row in set (25.21 sec)

在实际项目中,如果放在公网中,假如同时有1000个人并发查询,那很可能就死机。所以要对表创建索引:

--创建索引用时1分8秒
mysql> alter table EMP add index(empno);
Query OK, 0 rows affected (1 min 7.59 sec)

再次查找员工编号为998877的员工信息,花费0.04秒:

mysql> select * from EMP where empno=998877;
+--------+--------+----------+------+---------------------+---------+--------+--------+
| empno  | ename  | job      | mgr  | hiredate            | sal     | comm   | deptno |
+--------+--------+----------+------+---------------------+---------+--------+--------+
| 998877 | HJxoaj | SALESMAN | 0001 | 2023-06-24 00:00:00 | 2000.00 | 400.00 |    463 |
+--------+--------+----------+------+---------------------+---------+--------+--------+
1 row in set (0.04 sec)

二、磁盘

MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道,如何提升效率,是 MySQL 的一个重要话题。

磁盘的博客请点击本处:【Linux】缓冲区/磁盘inode/动静态库制作

三、MySQL、OS、磁盘的交互方式(InnoDB 存储引擎)

MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB (后面统一使用 InnoDB 存储引擎讲解)

mysql> show global status like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.06 sec)

1、MySQL服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。Buffer Pool单次向磁盘刷入1M数据和单次向磁盘刷入100M数据,效率肯定是不同的,MySQL底层也有自己的‘缓冲区’刷新策略来保证IO效率。

2、MySQL中的数据文件,是以page为单位保存在磁盘当中的。

3、MySQL的CURD操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。

4、而只要涉及计算,就需要CPU参与,为了便于CPU参与,一定要先将数据移动到内存当中。所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。

5、为了更高的效率,一定要尽可能的减少系统和磁盘IO的次数。

四、MySQL中索引和page的理解

磁盘这个硬件设备的基本单位是 512 字节(有些会更大),而 MySQL InnoDB引擎 和磁盘进行数据交互的基本单位是 16KB,这一个个的基本数据单位,在 MySQL 中叫做page(注意和系统的page区分)

MySQL中必然存在大量的page,为了先描述再组织,单个page内部除了用户数据之外,还存在一部分的数据结构,用于MySQL组织管理大量的page。

1、为什么MySQL和磁盘进行IO交互的时候,要采用page的方案进行交互,而不是采用用多少,加载多少的方式?

预加载,根据局部性原理,减少IO次数,提升效率。

IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数。

2、MySQL对page的管理方式

搞张测试表,记得加主键约束:

create table if not exists user (
id int primary key, --一定要添加主键,只有这样才会默认生成主键索引
age int not null,
name varchar(16) not null
);

mysql> desc user;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | NO   | PRI | NULL    |       |
| age   | int(11)     | NO   |     | NULL    |       |
| name  | varchar(16) | NO   |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.04 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)

为什么MySQL要帮我们把数据进行预排序?

2.1先来看个错误page的数据结构

MySQL每次加载一个page,每个page采用双向链表,对相邻page进行关联,page内部也使用链表进行连接,虽说可以满足CRUD的要求,但是数据量一大,面对地毯式的顺序查找会大大降低数据搜索效率。

2.2单个page内部的正确的数据结构

这个结构相较于本节2.1的结构,多花了一些空间存放目录。这目录就像看书、查字典一样,我们可以通过目录快速定位目标数据的大致页数,提升查找效率。所以MySQL的page里的这些目录虽然说多占用了一丢丢的空间,但大大提高了我们查找数据的速度(空间换时间)。

这也解释本段开头我们无序插入数据时,MySQL自动帮我们进行了排序。这是因为只有数据是有序的,MySQL才方便引入页目录,提升后续的查找效率。(页目录一定要有序!如果页目录是无序的,你代入计算机,如果你是计算机,面对无序的页目录,能快速定位数据吗?) 

2.3多个page之间的正确的数据结构

刚刚我们在每个page内部搞了页目录,减少了page内部的检索次数,提升了单page的搜索效率。

上图画出了多个page之间的连接关系,从图中看出页目录在多个page中也是呈现顺序关系的,如果是跨页搜索数据,也只能从前往后顺序遍历每个页的页目录,如果page一多,这种检索方式会大大降低页与页之间数据搜索速度,为了解决该问题,我们同样使用目录的方式对每个页中的目录进行管理:

上图同样使用了page的方式来管理页目录,每个page中不包含有效数据,只包含对应page中的始末页目录,所以一个“一级目录”可以管理上千个page。

那么问题又来了,如果底层的page很多,势必会造成一级目录的数量变多,那么我们对一级目录的遍历不是又变成了线性遍历了吗?再套一层:

一般两三层就够了,那些只用于索引的page,每个可都是16KB,单个page可以索引上千个下一层的page!完全够用,这样套了三层,即便面对海量数据,MySQL的查询效率也不低。(如果不够那就再套一层,存储数量指数级增长)后续查找时,自上而下进行查找,查到哪里就加载哪一部分的page,并不会加载整颗B+树到内存。

仔细看图会发现这就是一颗B+树。不过要注意:

1、并不是所有的存储引擎的索引都是采用B+树,还有哈希索引等方式。只能说主流的存储引擎是采用B+树作为索引的数据结构。

2、只有叶子结点采用链表进行级联,这是因为这是B+树的特性;同时,叶子结点进行级联可以满足范围查找(有时候数据读取的时候跨页了,叶子结点有指向next页的指针,就很方便)

2.4B+树的特征 

1、非叶节点不保存数据,只用来索引,所有数据都保存在叶子节点。

2、数据只在叶子结点保存,并保存指向前后叶子结点的指针,通过链表指针对叶子结点进行级联,且叶子结点本身依关键字的自小而大顺序连接。

2.5MySQL的主键

之前创建表的时候,我们指明看主键列,MySQL会根据主键进行排序。如果我们在创建表的时候没有指明主键,MySQL会生成一个隐藏的列作为主键。这也说明了我们指明主键,MySQL会按照主键进行排序,我们不指明主键,MySQL的排序以默认生成的主键为准,所以这时我们的数据插入时的顺序是怎样的,拿出来的顺序就是怎样。

本文第一张查找员工编号为998877的员工信息,花费25秒。这是因为该表在MySQL中是以默认主键进行索引构建的B+树,你拿着毫不相干的员工编号去查找,MySQL只能线性的去遍历,当然慢了。

后来我们按照员工编号重新构建了一颗B+树(辅助索引),再次按照编号去查找员工信息,发现找的飞快:

2.6为什么B+树做索引优于其他数据结构?

1、线性数据结构

线性数据结构如链表、顺序表,挨个挨个遍历,上文就是因为线性表效率低下的问题,一次次修改结构为B+树。

2、二叉搜索树

别忘了在学二叉搜索树的时候,这种数据结构的时间复杂度完全由查找分支的高度决定,最优的时间复杂度是O(lgN),但是二叉搜索树一旦歪一点,甚至可能退化为线性结构,这个时候时间复杂度将会大大提高。

3、红黑树和AVL树

这两种数据结构很优秀,如下图,查找八千万的数据最坏也仅需查找26次左右。但是红黑树和AVL树本质上都是二叉树,相同数据下树的高度会比B+树高,树的高度越高,单次查找所淘汰的数据量越少,效率越低。查找效率略逊于B+树。

4、哈希

官方的索引实现方式中, MySQL 的索引是支持HASH的,不过 InnoDB 和 MyISAM 并不支持。哈希的查找效率是O(1),但是它不支持范围查找。

5、B树

B树和B+树的区别在于:

1、B树的非叶节点中除了存放下一层的页目录,也会存放数据,这就导致了每个非叶节点存放的下一层的页目录变少,可能会增加整颗树的高度。

2、B树的叶节点之间不会采用链式结构进行连接。范围查找需要重新遍历整棵树。

2.7聚簇索引和非聚簇索引

MyISAM 存储引擎-主键索引

MyISAM 引擎同样使用B+树作为索引结果,和上一节讲的innodb存储引擎不一样的是,MyISAM叶节点的data域存放的是数据记录的地址。下图为 MyISAM表的主索引, Col1 为主键。

聚簇索引:像innodb存储引擎那样把B+树和数据存放在一起称为聚簇索引。

非聚簇索引:像MyISAM存储引擎那样把B+树和数据分离的方式称为非聚簇索引。

2.7.1MyISAM的辅助(普通)索引

MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。

对于 MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。

MyISAM存储引擎可以在一张表中建立多个索引,下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别:

2.7.2innodb的辅助(普通)索引

InnoDB 除了主键索引,用户也会建立辅助(普通)索引,我们以上表中的 Col3 建立对应的辅助索引:

InnoDB的非主键索引中叶子节点并没有数据,而只有对应记录的key值。所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询

为何InnoDB针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?数据主键索引有,没必要保存两份,不然太浪费空间了。

五、索引的操作

1、创建索引

1.1创建主键索引

方式一:建表时指明主键

-- 在创建表的时候,直接在字段名后指定 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);

1.2创建唯一索引(属于普通索引)

方式一:建表时指明唯一键

-- 在表定义时,在某列后直接指定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);

1.3创建普通索引/复合索引

方式一:建表时指明普通索引

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);

创建的复合索引其实是一颗B+索引,会发现name和email的普通键一样,复合索引的作用在于指定多个字段构建一颗B+树,如果需要高频的通过name找到email的操作,就可以构建复合索引,这样就避免了回表查询,通过索引找另一个索引的方式叫索引覆盖: 

mysql> alter table test1 add index(name,email);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0

*************************** 2. row ***************************
        Table: test1
   Non_unique: 1
     Key_name: name--索引名称是一样的,这俩是同一颗B+树
 Seq_in_index: 1
  Column_name: name
    Collation: A
  Cardinality: 0
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
      Comment: 
Index_comment: 
*************************** 3. row ***************************
        Table: test1
   Non_unique: 1
     Key_name: name--索引名称是一样的,这俩是同一颗B+树
 Seq_in_index: 2
  Column_name: emile
    Collation: A
  Cardinality: 0
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE
      Comment: 
Index_comment: 
3 rows in set (0.00 sec)

创建复合索引完成,后续可以使用name进行查找,也可以使用(name,email)进行查找,但是不能使用email进行查找,这是索引的最左匹配原则

1.4创建全文索引

当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。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 ...');

查询有没有database数据:

--普通查询
select * from articles where body like '%database%';
--全文索引
SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database');

2、查询索引

show keys from 表名;--方式一
show index from 表名;--方式二
desc 表名;----方式三,这种方式显示出来的信息比较简略

mysql> show index from test1\G;
*************************** 1. row ***************************
        Table: test1
   Non_unique: 0
     Key_name: PRIMARY--索引名称(B+树索引)
 Seq_in_index: 1
  Column_name: id--以那一列为索引构建的B+树
    Collation: A
  Cardinality: 0
     Sub_part: NULL
       Packed: NULL
         Null: 
   Index_type: BTREE--索引类型(B+树)
      Comment: 
Index_comment: 
1 row in set (0.00 sec)

3、删除索引

删除主键索引:

--方式一:删除主键索引
alter table 表名 drop primary key;

其他索引的删除(例如唯一索引的删除):

--方式二:索引名就是show keys from 表名中的 Key_name 字段
alter table 表名 drop index 索引名;
--方式三:
mysql> drop index name on user8;

4、什么字段应该创建索引 

1、有主键和唯一键约束的字段自带索引

2、某一列频繁的被作为查询条件

3、唯一性太差的列不适合作为索引,即使这一列被频繁查询

4、更新频繁的字段不适合作为索引

猜你喜欢

转载自blog.csdn.net/gfdxx/article/details/131404147