前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >MySQL基础篇4 mysql的索引

MySQL基础篇4 mysql的索引

原创
作者头像
历久尝新
修改2020-05-18 18:14:12
修改2020-05-18 18:14:12
48900
代码可运行
举报
文章被收录于专栏:学而时习之学而时习之
运行总次数:0
代码可运行

索引的出现其实就是为了提高数据查询效率, 就像书的目录一样

索引常见的三种模型

哈希表

三中key-value存储数据的结构, 哈希的思路很简单, 把值放在数组里, 用一个哈希函数把key换算成一个确定的位置, 然后把value放在数据的这个位置

不可避免的, 多个key经过哈希函数的换算, 会出现同一个值的情况, 处理这种情况的方法是拉出一个链表;

假设, 你现在维护着一个身份证信息和姓名的表, 表示根据身份证号查找对应的名字, 这时的哈希索引的示意图如下:

哈希表示意图
哈希表示意图

图中user2, user4 根据身份证号算出来的哈希值都是N, 但是么关系, 后面还跟了一个链表, 假设这时候你要查ID_CARD

_N2对应的名字是什么, 处理步骤就是:1. 通过Id_card_n2 通过哈希函数算出N 2. 按照顺序遍历, 找到name2

所以, 哈西表这种结构适用于只有等值查询的场景, eg memcached 以及其他一些nosql引擎

有序数组:

与哈希表不同, 有序数组在等值查询和范围查询中的性能都十分的优秀.

还是上面那个例子, if 我们用有序数组来实现的话, 示意图如下

有序数组示意图
有序数组示意图

假设, 我们身份证号没有重复, 这个数据就是按照身份证号递增的顺序保存的, 这时候如果你要查到id_card_n2对应的名字, 使用二分法就可以快速得到, 时间复杂度是O(log(N)).

同时很显然, 这个索引结构支持范围查询, 你要查身份证号在[ID_card_x, ID_card_y]区间的user, 可以先使用二分法找到ID_card_x(如果id_card_x 不存在, 则找到大雨id_card_x的第一个user), 然后向右遍历, 直到查到第一个大于id_card_y的身份证号, 然后退出循环.

如果只看查询效率, 有序数组就是最好的数据结构了, 但是在需要更新数据的时候就头皮发麻了, 你往中间插入一个记录, 就必须挪动后面所有的几句, 成本太高.

so 有序数组索引只适用于静态存储引擎, eg 你要保存的是2016某年个城市所有的人口信息, 这类不会再修改的数据.

搜索树:

如果用二叉搜索书来实现的话, 示意图如下:

二叉搜索树示意图
二叉搜索树示意图

二叉搜索数的特点是: 每个节点的左儿子小于父节点, 父节点又小于右儿子;

这样如需要查询id_card_2 的话, 按照图中的搜索顺序就是 userA -> userC -> userF -> user2, 时间复杂度是O(log(N));

当然为了维持O(log(N))的查询复杂度, 需要保持这个树是平衡二叉树, 为了做这个保证更新的时间复杂度也是O(log(N));

实际上大多数的数据库存储不使用二叉树, 因为索引不止只在内存中, 还需要写到磁盘中.

为了让一个查询尽量少的使用磁盘, 就必须让查询的过程访问尽量少的数据块, 我们不应该使用二叉树, 而是使用N叉数, N取决于数据块的大小.

以innodb的一个整数字段索引为例, 这个n差不多是1200, 这颗树高是4的时候, 就可以存1200的三次方这个值.考虑到树根的数据快总在内存中, 一个10亿行的表上一个整数字段的索引, 查找一个值最多只需要访问3次磁盘, 其实数的第二层也有很大概率在内存中, 那么访问磁盘的次数就更少了

N叉书由于在读写上的性能有点, 以及适配磁盘的访问模式, 已经广泛应用在数据库中.

innodb的索引模型

在innodb中, 表都是根据主键顺序以索引的形式存放的, 这种存储方式的表成为索引组织表, 又因为前面我们提到, innodb使用了b+树索引模型, 所以数据都是储存在b+树中的.

每一个索引在innodb里面对应一颗b+树.

假设,我们有一个主键列为id的表, 表中有字段k, 并且在k上有索引. 建表语句是:

代码语言:javascript
代码运行次数:0
复制
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),两棵树的示例示意图如下。

innodb的索引组织结构
innodb的索引组织结构

根据叶子节点的内容, 索引类型分为主键索引和非主键索引.

  • 主键索引的叶子节点存的是整行节点. 在innodb中, 主键索引也叫做聚簇索引. clustered index;
  • 非主键索引的叶子节点内容是主键的值. 在innodb中, 非主键索引也被叫做二级索引. secondary index.

一个问题: 主键索引和普通索引的查询有啥区别.

  • 如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
  • 如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。

也就是说, 基于非主键索引的查询需要多扫描一颗索引树, 因此我们应该尽量使用主键索引.

索引维护

b+树为了维护索引有序性, 在插入新值的时候需要做必要的维护.

以上图为例子

  • 如果要插入新的行id为700, 则只需要在R5的记录后插入一个新的记录
  • 如果插入的id值为400, 需要逻辑上挪动后面的数据,空出位置
  • 如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂

除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%

有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程

一个问题: 什么时候使用自增主键, 多会不用?

自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT

自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂

而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高

除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?

由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节

所以, 主键长度越小, 普通索引的叶子节点就越小, 普通索引占用的空间也就越小.

从性能和存储空间方面考量, 自增主键往往是更合理的选择.

那啥时候用业务字段做主键呢?

  1. 只有一个索引;
  2. 该索引必须是唯一索引;

典型的kv场景, 由于没有其他索引, 所以不用考虑其他索引的叶子节点大小. 用业务字段做主键, 可以避免每次搜索两棵树


再看一个问题:

代码语言:javascript
代码运行次数:0
复制
mysql> create table T (
ID int primary key,
k int NOT NULL DEFAULT 0, 
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;

insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

初始化一个表, 在这个表执行 select * from T where k between 3 and 5, 需要执行几次树的搜索操作?

innodb的索引组织结构
innodb的索引组织结构

执行流程:

  1. 在k索引树上找到k=3的记录, 取得id=300;
  2. 到id索引树上查到id=300, 对应r3;
  3. 在k索引树取下一个值k=5, 取得id=500;
  4. 再回到id索引树查到id=500, 对应r4;
  5. 在 k 索引树取下一个值 k=6,不满足条件,循环结束

回到主键索引树搜索的过程, 叫做回表. 上述过程读k索引3次(1,3,5), 回表两次(2, 4)

由于查询的结果所需的数据只在主键索引上有, 所以不得不回表, 如何避免回表呢?

覆盖索引

执行的语句是 select ID from T where k between 3 and 5 就是覆盖索引

由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

一个问题, 在一个市民信息表上, 是否有必要将身份证和名字建立联合索引??

假设这个市民表的定义是这样的:

代码语言:javascript
代码运行次数:0
复制
CREATE TABLE `tuser` ( 
`id` int(11) NOT NULL, 
`id_card` varchar(32) DEFAULT NULL, 
`name` varchar(32) DEFAULT NULL,
 `age` int(11) DEFAULT NULL, 
 `ismale` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`),
  KEY `id_card` (`id_card`), 
  KEY `name_age` (`name`,`age`)) ENGINE=InnoDB

如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。

最左前缀原则

b+树这种索引结构, 可以利用索引的最左前缀, 来定位记录

我们用(name, age) 这个联合索引来分析.

(name, age) 索引示意图
(name, age) 索引示意图

索引项是按照索引定义里面出现的字段数据排序的

当你的逻辑需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果

如果你要查的是所有名字第一个字是“张”的人,你的 SQL 语句的条件是"where name like ‘张 %’"。这时,你也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止

可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符

所以问题来了. 在建立联合索引的时候, 如何安排索引内的字段顺序?

主要的评估标准是索引的复用原理, 所以当已经有了(a, b)这个联合索引以后, 一般就不需要单独在a上建立索引了.

因此,

第一原则是, 如果通过调整顺序, 可以少维护一个索引, 那么这个顺序往往就是需要优先考虑采用的

第二原则是空间, 比如上面这个市民表的情况,name 字段是比 age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引

索引下推

如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

代码语言:javascript
代码运行次数:0
复制
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;

而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数

无索引下推执行流程
无索引下推执行流程
索引下推执行流程
索引下推执行流程

补充和小结

b+树可以很好的配合磁盘的读写特性, 减少单次查询的磁盘的访问次数.

满足语句需求的情况下, 尽量少的访问资源是数据库设计的重要原则.

在设计表结构时, 要以减少资源消耗为目标.

b+树相关补充

具体算法实现会专门再写一篇分析

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 索引常见的三种模型
    • 哈希表
    • 有序数组:
    • 搜索树:
  • innodb的索引模型
    • 根据叶子节点的内容, 索引类型分为主键索引和非主键索引.
    • 一个问题: 主键索引和普通索引的查询有啥区别.
  • 索引维护
  • 覆盖索引
  • 最左前缀原则
  • 索引下推
  • 补充和小结
    • b+树相关补充
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档