mysql 可以存数组_MySQL中的索引

903749e4fdd7c12a52eb553a8ca17deb.png

点击上方蓝字关注我们

8ed9c575b50147c7f9b852131e0c7c8c.png

a3224843701e60a1454f55fc83bcc372.png

索引是应用程序设计和开发的一种重要方面。可以这么理解索引相当于一本书的目录,让你快速定位到你想要的内容。但是索引大多,应用程序的性能可能会受到影响。索引太少,这对查询性能又会产生影响。因此,我们下面来介绍一下MySQL中InnoDB引擎的索引,让各位更好的理解。


常见的三种索引:哈希表、有序数组和搜索树

哈希表

哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的键即 key,就可以找到其对应的值即 Value。

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

例子

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

1015c3cd10053d2d35292a893ad664e8.png

我们发现:User2 和 User4 根据身份证号算出来的值都是 N,但没关系,后面还跟了一个链表

如果我们要查询ID_card_n2 对应的名字,那么执行步骤如下:

  1. 将 ID_card_n2 通过哈希函数算出 N;

  2. 按顺序遍历,找到 User2。

注意:图中四个 ID_card_n 的值并不是递增的,这样做的好处是增加新的 User 时速度会很快,只需要往后追加。

缺点:因为不是有序的,所以哈希索引做区间查询的速度是很慢的。因此如果我们要查找身份证号在[ID_card_X, ID_card_Y]这个区间的所有用户,就必须全部扫描一遍

所以,哈希表这种结构适用于只有等值查询的场景,比如 Memcached 及其他一些 NoSQL 引擎


有序数组

有序数组在等值查询和范围查询场景中的性能就都非常优秀

例子

我们还是上面那个根据身份证号查名字的例子,如果我们使用有序数组来实现的话,示意图如下所示:

c14da93b491d04c3fae501651576ea5d.png

这里我们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的

我们知道对于一个有序的数组,我们可以用二分查找来找指定的ID_card_n2 对应的名字,同时我们也可以用二分查找对身份证号在[ID_card_X, ID_card_Y]区间的 User 进行查找。这两个查询的复杂度是 O(log(N))。

缺点:如果我们在更新数据时需要插入一条记录,如果那条记录正好是插入在有序数组的尾部,那还好。但是往中间插入一个记录就必须得挪动后面所有的记录,成本太高。

所以,有序数组索引只适用于静态存储引擎


搜索树

二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;

例子

我们还是上面根据身份证号查名字的例子,如果我们用二叉搜索树来实现的话,示意图如下所示:

7885e66b02f42da2134e2a3d4868e34a.png

二叉搜索树的查询

我们都知道二叉搜索树的特点是:父节点左子树所有结点的值小于父节点的值右子树所有结点的值大于父节点的值

通过这个方法:如果我们要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径得到。这个时间复杂度是 O(log(N))。

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

树可以有二叉,也可以有多叉。多叉树就是每个节点有多个儿子,儿子之间的大小保证从左到右递增。

数据库存储并不使用二叉搜索树

  • 其原因是,索引不止存在内存中,还要写到磁盘上。你可以想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。

  • 因此一般使用“N叉树”,我们以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。


索引模型在 InnoDB 中

表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。

InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。每一个索引在 InnoDB 里面对应一棵 B+ 树

例子

我们假设有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引

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),两棵树的示例示意图如下。

35b9e27993c3a482ed53270c03849220.png

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

主键索引

主键(ID)索引的叶子节点存的是整行数据。在 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+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。

35b9e27993c3a482ed53270c03849220.png

页分裂

我们以上面那个图为例

  • 如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。

  • 如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。

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

页合并

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


案例分析

什么时候应该使用自增主键,什么时候不应该使用?

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

插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的 ID 值。也就是说,自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。

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

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

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

有没有什么场景适合用字段直接做主键的呢?

有些场景需求是这样的:

  1. 只有一个索引;

  2. 该索引必须是唯一索引。

这就是典型的 KV 场景。由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。这时候我们就要优先考虑上一段提到的“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。


InnoDB中非主键索引的查询

我们先创建一个表

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

创建出来的树索引是这样的

35b9e27993c3a482ed53270c03849220.png

现在我们来看看下面这条语句需要执行几次树的搜索操作,会扫描多少行?

 select * from T where k between 3 and 5
  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

这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表

  • 总结:也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

注意:在引擎内部使用覆盖索引在索引 k 上其实读了三个记录,R3~R5(对应的索引 k 上的记录项),但是对于MySQL 的 Server 层来说,它就是找引擎拿到了两条记录,因此 MySQL 认为扫描行数是 2

分析下面问题

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

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

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

我们知道,身份证号是市民的唯一标识。也就是说,如果有根据身份证号查询市民信息的需求,我们只要在身份证号字段上建立索引就够了。而再建立一个(身份证号、姓名)的联合索引,是不是浪费空间?

如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。当然,索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。


最左前缀原则

了解完覆盖索引我们可能有这些疑问

  1. 如果为每一种查询都设计一个索引,索引是不是太多了?

  2. 如果我现在要按照市民的身份证号去查他的家庭地址呢?

  3. 虽然这个查询需求在业务中出现的概率不高,但总不能让它走全表扫描吧?

  4. 反过来说,单独为一个不频繁的请求创建一个(身份证号,地址)的索引又感觉有点浪费。应该怎么做呢?

aa7fce8a87f4ac0b11837b5f5ce36e05.png

对于B+ 树这种索引结构,我们可以利用索引的“最左前缀”,来定位记录为了直观地说明这个概念,我们用(name,age)这个联合索引来分析(如上图)。

可以看到,索引项是按照索引定义里面出现的字段顺序排序的。

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

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

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

现在我们来想想下面这些问题

  • 在建立联合索引的时候,如何安排索引内的字段顺序?

这里我们的评估标准是,索引的复用能力。因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。所以现在你知道了,这段开头的问题里,我们要为高频请求创建 (身份证号,姓名)这个联合索引,并用这个索引支持“根据身份证号查询地址”的需求。

  • 如果既有联合查询,又有基于 a、b 各自的查询呢?

查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候你不得不维护另外一个索引,也就是说你需要同时维护 (a,b)、(b) 这两个索引。这时候,我们要考虑的原则就是空间了。比如上面这个市民表的情况,name 字段是比 age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引。


索引下推

我们讲到最左前缀可以用于在索引中定位记录。这时,我们可能要问,那些不符合最左前缀的部分,会怎么样呢?

还是以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

mysql> select * from tuser where name like '张%' and age=10 and ismale=1;

我们已经知道了前缀索引规则,所以这个语句在搜索索引树的时候,只能用 “张”,找到第一个满足条件的记录 ID3。

aa7fce8a87f4ac0b11837b5f5ce36e05.png

然后我们判断其他条件是否满足。此时我们引入索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

d2604f8d90f99e2b9ec6940d318b8697.png

1e181190525f3f1f2869a14a1a7c67a8.png

每一个虚线箭头表示回表一次。

第一幅图中因为在 (name,age) 索引里面没有了 age 的值,这个过程 InnoDB 并不会去看 age 的值,只是按顺序把“name 第一个字是’张’”的记录一条条取出来回表。因此,需要回表 4 次。

第二幅图中InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。


总结

三种常见索引 :哈希表、有序数组和搜索树

InnoDB中的索引模型:B+树

InnoDB中的索引:主键索引与非主键索引

索引的维护:页分裂和页合并

非主键索引的相关优化:覆盖索引、最左前缀索引、索引下推

总的来说:在满足语句需求的情况下, 尽量少地访问资源是数据库设计的重要原则之一。我们在使用数据库的时候,尤其是在设计表结构时,也要以减少资源消耗作为目标。


问题1

mysql> create table T(    id int primary key,     k int not null,     name varchar(16),    index (k))engine=InnoDB;

对于上面例子中的 InnoDB 表 T,如果要重建索引 k,两个 SQL 语句可以这么写:

alter table T drop index k;alter table T add index(k);

如果要重建主键索引,也可以这么写:

alter table T drop primary key;alter table T add primary key(id);

对于上面这两个重建索引的作法,说出自己的理解。如果有不合适的,为什么,更好的方法是什么?


答案1

可能有人会问为什么要重建索引。我们文章的页分裂以及页合并里面有提到,索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。

对于这道题,重建索引 k 的做法是合理的,可以达到省空间的目的。但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,可以用这个语句代替 :alter table T engine=InnoDB。


问题2

实际上主键索引也是可以使用多个字段的。,有这么一个表,表结构定义类似这样的:

CREATE TABLE `geek` (  `a` int(11) NOT NULL,  `b` int(11) NOT NULL,  `c` int(11) NOT NULL,  `d` int(11) NOT NULL,  PRIMARY KEY (`a`,`b`),  KEY `c` (`c`),  KEY `ca` (`c`,`a`),  KEY `cb` (`c`,`b`)) ENGINE=InnoDB;

由于历史原因,这个表需要 a、b 做联合主键。但是,既然主键包含了 a、b 这两个字段,那意味着单独在字段 c 上创建一个索引,就已经包含了三个字段了呀,为什么要创建“ca”“cb”这两个索引?有人说是因为有下面这样的两种语句:

select * from geek where c=N order by a limit 1;select * from geek where c=N order by b limit 1;

请问这个解释对吗?为了这两个查询模式,这两个索引是否都是必须的?为什么呢?


答案2

表记录

–a--|–b--|–c--|–d-- 1    2    3    d 1    3    2    d 1    4    3    d 2    1    3    d 2    2    2    d 2    3    4    d

主键 a,b 的聚簇索引组织顺序相当于 order by a,b ,也就是先按 a 排序,再按 b 排序,c 无序。

索引 ca 的组织是先按 c 排序,再按 a 排序,同时记录主键

–c--|–a--|–主键部分b-- (注意,这里不是 ab,而是只有 b) 2    1     3 2    2     2 3    1     2 3    1     4 3    2     1 4    2     3

这个跟索引 c 的数据是一模一样的。

索引 cb 的组织是先按 c 排序,在按 b 排序,同时记录主键

–c--|–b--|–主键部分a-- (同上) 2    2     2  2    3     1 3    1     2 3    2     1 3    4     1 4    3     2

对于下面的语句

select ... from geek where c=N order by a

走ca,cb索引都能定位到满足c=N主键,而且主键的聚簇索引本身就是按order by a,b排序,无需重新排序。所以ca可以去掉。

select ... from geek where c=N order by b

这条sql如果只有 c单个字段的索引,定位记录可以走索引,但是order by b的顺序与主键顺序不一致(因为主键索引排序是先排a后排b),这就需要额外排序。因此cb索引可以把排序优化掉。

所以,结论是 ca 可以去掉,cb 需要保留。

点个在看 你最好看

39b5b1165159045371a6ea59890923a1.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值