索引
@(数据库)[MySQL]
索引是什么?
索引的解释
数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询,更新数据库表中数据。
- 值得注意的是:SQL 标准并没有为数据库用户提供任何在数据库系统中控制创建和维护索引的防范,由于索引是冗余的数据结构,因此索引对保证正确性来说并不是必须的;但是索引对事务的高效处理十分重要,对完整性约束的有效实施也很重要;如果没有索引的话,MySQL进行全表扫描会对资源进行极大的浪费。
- 如果不恰当的使用索引的话,只会增加无效的数据维护,耗费资源,同时查询效率仍然得不到提高。
- 那么索引究竟是什么呢?索引是一种数据结构,一个索引是存储的表中一个特定列或者某几个特定列的值数据结构,索引是在列上创建的。
索引的分类
有两种基本的索引类型
顺序索引:基于值的顺序排序
散列索引:基于将值平均分布在若干散列桶中,一个值所属的散列通是有一个函数决定的,该函数被称为散列函数
顺序索引
顺序索引中,根据包含记录是否按照搜索码制定的顺序排序可以分为聚集索引和非聚集索引
- 被索引文件中的记录自身也可以按照某种排序顺序存储,一个文件可以有多个索引,分别预计不同的搜索码,如果包含记录的文件按照某个搜索码指定的顺序排序,那么改搜索码对应的索引称为聚集索引,也称为主索引(Primary Index)。
- 搜索码指定的顺序于文件中记录的物理顺序不同的索引被称为非聚集索引或者辅助索引。
- 非聚集索引和聚集索引的区别在于,通过聚集索引可以查到需要查找的数据,而通过非聚集索引可以查到记录对应的主键值, 再使用主键的值通过聚集索引查找到需要的数据
- 关于数据库中 Key,Primary Key,Unique Key 与 index 的不同可以参见这篇文章
- 关于顺序索引,一般如果没有明确表示的话指的就是 B+Tree 索引
哈希索引
- 哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有咧的查询才有效;对于每一行数据,存储引擎都会对所有的索引计算一个哈希码,哈希吗是一个较小的值,并且不同键值计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
聚簇索引和非聚簇索引并不是单独的索引类型,而是一种数据存储方式 [高性能 MySQL]
索引的优点和缺点
优点
- 索引大大减少减少了服务器需要扫描的数据量
- 索引可以帮助服务器避免排序和临时表
- 索引可以将随机 I/O 变为顺序 I/O
缺点
索引的缺点主要是针对不合理索引而言的,对于开发者而言,索引维护所耗费的资源和索引所提供的快速查询能力节省的时间资源两者进行取舍。
- 创建和维护索引都需要耗费时间,而且随着数据量的增加而增加。
- 每个索引还需要耗费额外的物理空间资源。
- 当对表中的数据进行 CURD 操作的时候,索引也需要对应的动态维护,增加数据维护成本。
索引的数据结构
MySQL 存储索引的时候一般我们没有明确之处其余结构就是指的是 B-Tree 数据结构存储索引。参考资料卫星数据:指的是索引元素所指向的数据记录,比如数据库的某一行。
磁盘 I/O
- 动态查询树主要有:二叉查找树,平衡二叉查找树,红黑树,B-Tree/B+Tree/B*Tree
- 前三者是典型的二叉查找树结构,其查找的时间复杂度O(log2N)与树的深度相关,那么降低树的深度自然对查找效率是有所提高的;还有一个实际问题:就是大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下,那么如何减少树的深度,一个基本的想法就是:采用多叉树结构(由于树节点元素数量是有限的,自然该节点的子树数量也就是有限的)。
- 关于硬盘(外存储器)的解读,磁盘获取数据由三部分时间组成:查找时间(代价最高),等待时间,传输时间.
- 磁盘读取数据是以盘块(block)为基本单位的。位于同一盘块中的所有数据都能被一次性全部读取出来。而磁盘IO代价主要花费在查找时间Ts上。因此我们应该尽量将相关信息存放在同一盘块,同一磁道中。或者至少放在同一柱面或相邻柱面上,以求在读/写信息时尽量减少磁头来回移动的次数,避免过多的查找时间Ts。
- 当我们利用索引查询的时候,显然不可能把整个索引全部加载到内存中,只能逐一加载每一个磁盘页,这里的磁盘页对应这索引树的节点.
- 在使用树结构查询的时候如果使用二叉树,那么磁盘的 IO 次数最坏情况下等于索引树的高度.此时索引树的高度就对查询资源至关重要.
B-Tree Wiki
什么是 B-Tree?一个 m 阶的 B 树有以下几个特征。
- 根节点至少有两个子节点
- 每个中间节点都包含 K-1个元素和 K 个子节点,其中 m/2<=k<=m
- 每一个叶子节点都包含 K-1个元素,其中 m/2<=k<=m
- 所有的叶子节点都位于同一层
- 每个非终端节点中包含有 n 个关键词信息(n,P0,K1,P1,K2....Kn,Pn)
a. 其中 n 的取值范围是ceil(m/2)-1<=n<=m-1
b. Ki为关键字,且关键字按照顺序排序(K(i-1)<Ki)
c. Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。[每个节点中的元素从小到大排列,节点当中 K-1个元素正好是 K 个孩子包含元素的值域划分]
模拟查找文件29的过程:(1) 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘IO操作1次】
(2) 此时内存中有两个文件名17,35和三个存储其他磁盘页面地址的数据。根据算法我们发现17<29<35,因此我们找到指针p2。
(3) 根据p2指针,我们定位到磁盘块3,并将其中的信息导入内存。【磁盘IO操作2次】
(4) 此时内存中有两个文件名26,30和三个存储其他磁盘页面地址的数据。根据算法我们发现26<29<30,因此我们找到指针p2。
(5) 根据p2指针,我们定位到磁盘块8,并将其中的信息导入内存。【磁盘IO操作3次】
(6) 此时内存中有两个文件名28,29。根据算法我们查找到文件29,并定位了该文件内存的磁盘地址。
分析上面的过程,发现需要3次磁盘IO操作和3次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。至于3次磁盘IO操作时影响整个B-tree查找效率的决定因素。
当然,如果我们使用平衡二叉树的磁盘存储结构来进行查找,磁盘IO操作最少4次,最多5次。而且文件越多,B-tree比平衡二叉树所用的磁盘IO操作次数将越少,效率也越高。
B 树中每个节点都具有卫星数据
节点的插入
插入(insert)操作:插入一个元素时,首先在B-tree中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。
例:自顶向下查找4的节点位置,发现4应当插入到节点元素3,5之间
节点3,5已经是两元素节点,无法再增加。父亲节点 2, 6 也是两元素节点,也无法再增加。根节点9是单元素节点,可以升级为两元素节点。于是拆分节点3,5与节点2,6,让根节点9升级为两元素节点4,9。节点6独立为根节点的第二个孩子
节点的删除操作
删除(delete)操作:首先查找B-tree中需删除的元素,如果该元素在B-tree中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素到父节点中,然后是移动之后的情况;如果没有,直接删除后,移动之后的情况.。删除元素,移动相应元素之后,如果某结点中元素数目小于ceil(m/2)-1,则需要看其某相邻兄弟结点是否丰满(结点中元素个数大于ceil(m/2)-1),如果丰满,则向父节点借一个元素来满足条件;如果其相邻兄弟都刚脱贫,即借了之后其结点数目小于ceil(m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点,以此来满足条件。那咱们通过下面实例来详细了解吧。
例:删除11节点
删除11后,节点12只有一个孩子,不符合B树规范。因此找出12,13,15三个节点的中位数13,取代节点12,而节点12自身下移成为第一个孩子。(这个过程称为左旋)
B+Tree
B+Tree 是对于 B-Tree 的一种变体,有着比 B-Tree 更高的查询效率。
一个 m 阶的 B 树有着如下特点
- 有 K 个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
- 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
根节点的最大元素等荣誉整个树的最大元素,叶子节点包含了全量元素信息,并且每一个叶子节点都带有指向下一个节点的指针,形成了一个有序链表.
在 B+Tree 树中,只有叶子节点保存卫星数据.
需要补充的是,在数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。
由于 B+Tree 树的中间节点没有卫星数据,所以同样大小的磁盘也(1~4K)可以容纳更多的元素,意味着查询的 IO 次数越少。
另外,B+Tree 的查询必须最终查询到叶子节点,和 B-Tree 不同,查询性能最稳定,而且在范围查询中,比起只能以来繁琐的中序遍历的 B 树,更有效率。
综合来看,B+Tree 对比 B-Tree 有三大优势:
- 单一节点存储更多的元素,使得查询的IO次数更少。
- 所有查询都要查找到叶子节点,查询性能稳定。
- 所有叶子节点形成有序链表,便于范围查询。
哈希索引
关于哈希索引,目前 MySQL 中只有 Memory 和 NDB 两种引擎支持,详细了解可以参考这篇文章MySQL索引之哈希索引,本文不在赘述。
索引的使用
理解了索引的数据结构之后我们就理解了索引在创建和使用上的一些方法(以下所描述的索引均指的是 B-Tree 索引)。
create table People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gendar enum('m','f') not null,
key(last_name, first_name, dob)
);
- 使用 B-Tree 索引的查询类型适用于下列集中情况
- 全值匹配
select * from People where last_name = 'Allen' and first_name = 'Cuba' and dob = '1999-01-01';
- 匹配最左前缀
select * from People where last_name = 'Allen';
- 匹配列前缀
select * from People where last_name like 'A%';
- 匹配范围值
select * from People where last_name > 'Allen' and last_name < 'Bob';
- 精确匹配某一列并范围匹配另外一列
select * from People where last_name = 'Allen' and last_name like 'B%';
-
只访问索引的查询
select last_name,
first_name, dob
from People
where last_name = 'Allen'and first_name = 'Cuba' and dob = '1999-01-01';
- 关于使用索引的一些限制
- 如果不是按照索引的最左列开始查找,则无法使用索引
select * from People where first_name = 'Allen';
- 不能跳过索引中的列
select * from People where last_name = 'Allen' and dob = '1999-01-01';
- 如果查询中有某个列的范围查询,则其右边的所有咧都无法使用索引优化查询
select * from People where last_name = 'Allen' and first_name like 'J%' and dob = '1999-01-01';
高性能的索引策略
- 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
- 使用独立的列
- 多列索引:MySQL 从5.0之后的更新版本引入了一种叫索引合并的策略,关于这项策略可以参考MySQL 优化之 index merge
- =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
- 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录
- 索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);
- 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
一些有价值的参考资料
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。