索引就类似书本的目录,作用就是方便我们更加快速的查找到想要的数据。
索引的实现方式比较多,常见的有哈希表
,有序数组
,搜索树
。
哈希表
是将数据以key-value
的形式存储起来,简单来说就是将key
通过哈希函数换算成数组中的一个确定的位置,将value
存到这个位置去。当key
比较多时,有可能换算出相同的位置,此时可以通过链表来解决。在查询时先找到位置,再对该位置的多个value
进行遍历。
哈希表适合用于等值查询,由于是无序的,不适合用来做区间查询。
有序数组
在等值查询和区间查询上效率都很高。由于是有序的,可以通过二分法快速得到结果。也支持范围查询。但是也有一个缺点,如果要在中间插入一个数据,那么后面的所有记录都要向后挪一位,成本太高了。
因此,有序数组只适用于静态存储引擎。 例如我们要保存2019年的出生人口信息,就适合用有序数组。
常见的搜索树有二叉
,也有多叉
。
二叉树
的特点是:
多叉树
的特点是:
由于索引不止存在内存中,还会写到磁盘上,而读磁盘越多,查询效率越慢。要降低读磁盘的次数的话,就要尽量访问尽量少的数据块。
假设数据块大小是N
,树高为M
,最多可以存的数据行数为 N^(M-1)
(N
的 M-1
次方)。最多访问磁盘数为 M-1
。
要使树高比较小,访问次数就少,N叉树
的树高就小于二叉树
。以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200,这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿行记录了。一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。
数据库底层存储的核心就是基于这些数据模型的。每碰到一个新数据库,我们需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景。
索引组织表
。因此,每一个索引在 InnoDB 里面对应一棵 B+ 树。
根据字段约束,分为主键索引
和普通索引
;根据字段内容是否可重复,分为唯一索引
和非唯一索引
。
主键索引
主键是一种约束,一个表中只能有一个主键;
主键可以是多个列;
主键可以被其它表引用为外键使用;
主键索引可以理解为非空字段
+唯一索引
;
主键索引的叶子节点存的是整行数据。
普通索引(二级索引)
一个表中可以有多个普通索引;索引可以有多列;
普通索引的叶子节点内容是主键的值;
唯一索引
字段内容不能重复,但是可以为空;
一个表中可以有多个唯一索引;
不能做外键使用;
非唯一索引
字段内容允许重复;
下面以表为例,建表语句:
mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k))engine=InnoDB;
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下:id
字段为主键索引
,主键索引
的字段是不会重复的,必定是唯一索引
;k
字段为普通索引
,k
的值允许重复,因此是非唯一索引
。
分析下面 2 条 SQL 语句:
select * from T where ID=500
。此时用到的是主键索引,因此直接从索引中返回了整行记录,只需要搜索ID
这棵 B+ 树。select * from T where k=5
。此时用到的是普通索引,需要先搜索 k
索引树,得到ID = 500
,再根据500
到ID
索引树搜索一次。这种需要返回主键索引树搜索的过程,叫做回表。以上两条 SQL 语句返回的结果是一样的,但是效率却不一样,因为第 2 条 SQL 语句有一次回表操作,效率会慢很多,因此,要尽量避免回表操作,多使用主键查询。
还是以上表为例,如果我们要插入一个数据,ID 值为 700,则只需要在 R5 后面新增加 1 条记录即可。如果插入的值 ID 为 400,那就需要逻辑上挪动后面的数据,空出位置。
如果恰好 R5 所在的数据页已经满了,那么就需要申请一个新的数据页,并且将 R5 挪过去,这个情况就叫做页分裂
。
数据页中并不是要利用率达到 100% 才会申请新的数据页。也不是说只要有数据删除,那么后一页的数据就会顺补到前一页,这样太浪费性能了。数据页有一个利用率,假设分裂是80%,合并是 50%。只要利用率达到了 80%,就会申请一个新的数据页。如果删除数据比较多,利用率低于 50% 了,就会把后一页的数据合并过来。
如何避免页分裂造成的性能消耗?常见做法是在表中,设置一个自增长的 id 主键,这个字段不能和业务相关。自增主键的定义:NOT NULL PRIMARY KEY AUTO_INCREMENT
。
这样每次插入数据,如果不指定 id 值,就会自增长到最后,因为和业务无关,所以没必要去指定 id 值。这样可以避免出现页分裂。
还是以上表为例,执行以下 SQL 语句,分析执行过程:
mysql> select * from T where k between 3 and 5;
k
上遍历,得到k=3
对应的 ID
值 300
;ID=300
去主键索引上取得整行记录R3
;k
,得到k=5
对应的 ID
值 500
;ID=500
去主键索引上取得整行记录R5
;k
,发现k=6
,不满足between
条件,循环结束。可以看到,这个过程读了k
索引树的 3
条记录(步骤 1,3,5), 回表了2
次(步骤2,4)。
如果我们换成以下 SQL 语句:
mysql> select ID from T where k between 3 and 5;
由于 ID
已经在k
索引树上了,因此可以直接返回结果,不用回表。这种索引中已经覆盖了我们要查询的数据,叫做覆盖索引
。
覆盖索引
可以减少树的搜索次数(没有回表过程),显著提高查询性能。
MySQL 认为上述操作扫描的行数是 2 行,因为在索引中查数据,是在引擎层的操作。而 Server 层最后只拿到了 2 条记录,因此 MySQL 认为只扫描了 2 行。
那么如何看扫描函数呢?有 2 种方法:
explain
查看预计扫描行数mysql> explain select * from t where a between 1000 and 2000;
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+
| 1 | SIMPLE | t | range | a | a | 5 | NULL | 1000 | Using index condition |
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+
1 row in set (0.01 sec)
mysql>
可以看到使用了索引 key=a
,预计扫描行数rows=1000
。
# Time: 191228 13:03:16
# User@Host: federated[federated] @ [60.191.76.22] Id: 177
# Query_time: 31.211439 Lock_time: 0.000059 Rows_sent: 0 Rows_examined: 95324
SET timestamp=1577509396;
CALL Z10004();
可以看到,扫描行数为Rows_examined: 95324
举一个例子来理解最左前缀原则,假设有一个联合索引(name,age)如下:
可以看到,索引顺序先按照第一个字段排序,再按照第二个字段。
假设我们要查询所有名为张三
的数据。可以快速定位到ID4
,再依次向后遍历。如果要查询所有姓张
的(where name like '张%')
,也能用到索引,先定位到ID3
,再依次向后遍历,直到不满足条件为止。
不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
在建立联合索引时,如何确定字段的前后顺序呢?
第一原则,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
比如,已经有了一个(a, b)索引,就不必再建立一个 a 索引了。
考虑磁盘空间占用大小。
比如,(name, age) 索引加上 age 索引,和 (age, name) 索引加上 name 索引。这两种情况,我们就要考虑占用空间了。选择占用空间小的。
由于name 字段比 age 字段大,因此我们选择(name, age) 索引加上 age 索引。
索引下推功能是在 MySQL 5.6 引入的,目的是减少回表次数。
还是以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
ID3
,然后回表到主键索引,找出对应的数据行,判断是否符合and age=10 and ismale=1
。最终要回表 4 次(ID3,ID4,ID5,ID6),返回的结果只有 ID4,ID5。数据库是以一定方式储存在一起、能与多个用户共享、具有尽可能小的冗余度、与应用程序彼此独立的数据集合,可视为电子化的文件柜——存储电子文件的处所,用户可以对文件中的数据进行新增、查询、更新、删除等操作。
¥199.00
¥798.00
¥199.00
¥48.00¥180.00
¥29.90
¥48.00¥180.00