索引的出现其实就是为了提高数据查询效率, 就像书的目录一样
三中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使用了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),两棵树的示例示意图如下。
也就是说, 基于非主键索引的查询需要多扫描一颗索引树, 因此我们应该尽量使用主键索引.
b+树为了维护索引有序性, 在插入新值的时候需要做必要的维护.
以上图为例子
除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%
有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程
一个问题: 什么时候使用自增主键, 多会不用?
自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT
自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂
而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高
除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节
所以, 主键长度越小, 普通索引的叶子节点就越小, 普通索引占用的空间也就越小.
从性能和存储空间方面考量, 自增主键往往是更合理的选择.
那啥时候用业务字段做主键呢?
典型的kv场景, 由于没有其他索引, 所以不用考虑其他索引的叶子节点大小. 用业务字段做主键, 可以避免每次搜索两棵树
再看一个问题:
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, 需要执行几次树的搜索操作?
执行流程:
回到主键索引树搜索的过程, 叫做回表. 上述过程读k索引3次(1,3,5), 回表两次(2, 4)
由于查询的结果所需的数据只在主键索引上有, 所以不得不回表, 如何避免回表呢?
执行的语句是 select ID from T where k between 3 and 5 就是覆盖索引
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
一个问题, 在一个市民信息表上, 是否有必要将身份证和名字建立联合索引??
假设这个市民表的定义是这样的:
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) 这个联合索引来分析.
索引项是按照索引定义里面出现的字段数据排序的
当你的逻辑需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果
如果你要查的是所有名字第一个字是“张”的人,你的 SQL 语句的条件是"where name like ‘张 %’"。这时,你也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止
可以看到,不只是索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符
所以问题来了. 在建立联合索引的时候, 如何安排索引内的字段顺序?
主要的评估标准是索引的复用原理, 所以当已经有了(a, b)这个联合索引以后, 一般就不需要单独在a上建立索引了.
因此,
第一原则是, 如果通过调整顺序, 可以少维护一个索引, 那么这个顺序往往就是需要优先考虑采用的
第二原则是空间,
比如上面这个市民表的情况,name 字段是比 age 字段大的 ,那我就建议你创建一个(name,age) 的联合索引和一个 (age) 的单字段索引
如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数
b+树可以很好的配合磁盘的读写特性, 减少单次查询的磁盘的访问次数.
满足语句需求的情况下, 尽量少的访问资源是数据库设计的重要原则.
在设计表结构时, 要以减少资源消耗为目标.
具体算法实现会专门再写一篇分析
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。