2021-05-13-MySQL 索引总结
MySQL 索引总结
生活中的索引
MySQL
官方对索引的定义为:索引Index
是帮助 MySQL
高效获取数据的 数据结构。可以得到索引的本质:索引是数据结构。
上面的理解比较抽象,举一个例子,平时看任何一本书,首先看到的都是目 录,通过目录去查询书籍里面的内容会非常的迅速。
MySQL中的索引
InnoDB
存储引擎支持以下几种常见的索引:B+树索引、全文索引、哈希索引,其中比较关键的是 B+
树索引,其他的索引有:聚集索引/聚簇索引、辅助索引/二级索引、联合索引/复合索引、覆盖索引/索引覆盖、自适应哈希索引、全文检索之倒排索引。
也有这么分类的:
普通索引:即一个索引只包含单个列,一个表可以有多个单列索引。 |
HashMap 适合做数据库索引吗?
1、hash
表只能匹配是否相等,不能实现范围查找;
2、当需要按照索引进行 order by
时,hash
值没办法支持排序;
3、组合索引可以支持部分索引查询,如(a,b,c)
的组合索引,查询中只用到了
a
和 b
也可以查询的,如果使用 hash
表,组合索引会将几个字段合并 hash
,没
办法支持部分索引;
4、当数据量很大时,hash
冲突的概率也会非常大。
B+Tree
B+
树索引就是传统意义上的索引,这是目前关系型数据库系统中查找最常用 和最为有效的索引。B+
树索引的构造类似于二叉树,根据键值(Key Value)快速 找到数据。注意 B+
树中的 B
不是代表二叉(binary),而是代表平衡(balance),因 为 B+
树是从最早的平衡二叉树演化而来,但是 B+
树不是一个二叉树。
在了解 B+
树索引之前先要知道与之密切相关的一些算法与数据结构,这有 助于更好的理解 B+
树索引的工作方式,因为 B+
树是通过二叉查找树,再由 平衡二叉树,B
树演化而来。
二分查找
二分查找法(binary search) 也称为折半查找法,用来查找一组有序的记录数组中的某一记录。
其基本思想是:将记录按有序化(递增或递减)排列,在查找过程中采用跳跃式方式查找, 即先以有序数列的中点位置作为比较对象,如果要找的元素值小于该中点元素,则将待查序 列缩小为左半部分,否则为右半部分。通过一次比较,将查找区间缩小一半。
给出一个例子,注意该例子已经是升序排序的,且查找 数字 48 |
二叉树
每个节点至多只有二棵子树;
- 二叉树的子树有左右之分,次序不能颠倒;
- 一棵深度为 k,且有 个节点,称为满二叉树(
Full Tree
); - 一棵深度为 k,且 root 到 k-1 层的节点树都达到最大,第 k 层的所有节点都 连续集中 在 最左边,此时为完全二叉树(
Complete Tree
)
平衡二叉树(AVL-树)
- 左子树和右子树都是平衡二叉树;
- 左子树和右子树的高度差绝对值不超过 1;
平衡二叉树:
非平衡二叉树:
平衡二叉树的遍历 |
平衡二叉树的旋转比较复杂,提供个博客专门详解:
平衡二叉树【旋转的超详细图解】
B+树
B+树是 B 树的一种变形形式,B+树上的叶子结点存储关键字以及相应记录的地址,叶子结点以上各层作为索引使用。一棵 m 阶的 B+树定义如下:
每个节点最多可以有 m 个元素;
除了根节点外,每个节点最少有 (m/2) 个元素;
如果根节点不是叶节点,那么它最少有 2 个孩子节点;
所有的叶子节点都在同一层;
一个有 k 个孩子节点的非叶子节点有 (k-1) 个元素,按升序排列;
某个元素的左子树中的元素都比它小,右子树的元素都大于或等于它;
非叶子节点只存放关键字和指向下一个孩子节点的索引,记录只存放在叶子节点中;
相邻的叶子节点之间用指针相连。
B+
树的变体为 B*
树,在 B+
树的非根和非叶子结点再增加指向兄弟的指针;B*
树定义了非叶子结点关键字个数至少为(2/3)*M
,即块的最低使用率为 2/3
(代替 B+
树的 1/2
)。
概要的了解下 B
树和 B+
树。
B+
树是为磁盘或其他直接存取辅助设备设计的一种平衡查找树。在 B+
树中, 所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点 指针进行连接。比如:
InnoDB中的索引
聚集索引/聚簇索引
InnoDB
中使用了聚集索引,就是将表的主键用来构造一棵 B+树,并且将整 张表的行记录数据存放在该 B+树的叶子节点中。也就是所谓的索引即数据,数 据即索引。由于聚集索引是利用表的主键构建的,所以每张表只能拥有一个聚集 索引。
聚集索引的叶子节点就是数据页。换句话说,数据页上存放的是完整的每行 记录。因此聚集索引的一个优点就是:通过过聚集索引能获取完整的整行数据。 另一个优点是:对于主键的排序查找和范围查找速度非常快。
如果没有定义主键呢?MySQL
会使用唯一性索引,没有唯一性索引, MySQL
也会创建一个隐含列 RowID
来做主键,然后用这个主键来建立聚集索引。
辅助索引/二级索引
上边介绍的聚簇索引只能在搜索条件是主键值时才能发挥作用,因为 B+树 中的数据都是按照主键进行排序的,那如果想以别的列作为搜索条件怎么 办?一般会建立多个索引,这些索引被称为辅助索引/二级索引。
对于辅助索引(Secondary Index
,也称二级索引、非聚集索引),叶子节点 并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索 引行中还包含了一个书签( bookmark)。该书签用来告诉 InnoDB 存储引擎哪里可 以找到与索引相对应的行数据。因此 InnoDB 存储引擎的辅助索引的书签就是相 应行数据的聚集索引键。
比如辅助索引 index(node)
,那么叶子节点中包含的数据就包括了(主键、 note
)。
回表
辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB
存储引擎会遍历辅助索引 并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引(聚集索引) 来找到一个完整的行记录。这个过程也被称为回表。也就是根据辅助索引的值查询一条完整的用户记录需要使用到 2
棵 B+
树,即一次辅助索引,一次聚集索引。
为什么还需要一次回表操作呢?直接把完整的用户记录放到辅助索引 d
的叶子节点不就好了么?如果把完整的用户记录放到叶子节点是可以不用回表, 但是太占地方了,相当于每建立一棵 B+
树都需要把所有的用户记录再都拷贝一 遍,这就有点太浪费存储空间了。而且每次对数据的变化要在所有包含数据的索 引中全部都修改一次,性能也非常低下。
很明显,回表的记录越少,性能提升就越高,需要回表的记录越多,使用二 级索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用二级索引。
那什么时候采用全表扫描的方式,什么时候使用采用二级索引 + 回表的方 式去执行查询呢?这个就是查询优化器做的工作,查询优化器会事先对表中的记 录计算一些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要 回表的记录数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于 使用二级索引 + 回表的方式。
联合索引/复合索引
前面对索引的描述,隐含了一个条件,那就是构建索引的字段只有一个, 但实践工作中构建索引的完全可以是多个字段。所以,将表上的多个列组合起来 进行索引称之为联合索引或者复合索引,比如 index(a,b)
就是将 a
,b
两个 列组合起来构成一个索引。
千万要注意一点,建立联合索引只会建立 1 棵 B+树,多个列分别建立索引 会分别以每个列则建立 B+
树,有几个列就有几个 B+树,比如,index(note)
、 index(b)
,就分别对 note
,b
两个列各构建了一个索引。
index(note,b)在索引构建上,包含了两个意思:
1、先把各个记录按照 note 列进行排序。
2、在记录的 note 列相同的情况下,采用 b 列进行排序。
覆盖索引/索引覆盖
既然多个列可以组合起来构建为联合索引,那么辅助索引自然也可以由多个 列组成。
InnoDB
存储引擎支持覆盖索引(covering index
,或称索引覆盖),即从辅 助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录。使用覆盖索 引的一个好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索 引,因此可以减少大量的 IO
操作。所以记住,覆盖索引并不是索引类型的一种。
自适应哈希索引
InnoDB
存储引擎除了前面所说的各种索引,还有一种自适应哈希索引, 知道 B+
树的查找次数,取决于 B+
树的高度,在生产环境中,B+
树的高度一般为 3~4
层,故需要 3~4
次的 IO
查询。
所以在 InnoDB
存储引擎内部自己去监控索引表,如果监控到某个索引经常 用,那么就认为是热数据,然后内部自己创建一个 hash
索引,称之为自适应哈 希索引( Adaptive Hash Index,AHI
),创建以后,如果下次又查询到这个索引, 那么直接通过 hash 算法推导出记录的地址,直接一次就能查到数据,比重复去 B+ Tree
索引中查询三四次节点的效率高了不少。
InnoDB
存储引擎使用的哈希函数采用除法散列方式,其冲突机制采用链表 方式。注意,对于自适应哈希索引仅是数据库自身创建并使用的,并不能对 其进行干预。通过命令 show engine innodb status
可以看到当前自适应哈希 索引的使用状况,测试查询结果如下:
|
哈希索引只能用来搜索等值的查询,如 SELECT* FROM table WHERE index co=xxx
。而对于其他查找类型,如范围查找,是不能使用哈希索引的, 因此这里出现了 non- hash searches/s
的情况。通过 hash searches: non- hash searches
可以大概了解使用哈希索引后的效率。
由于 AHI
是由 InnoDB
存储引擎控制的,因此这里的信息只供参考。不 过可以通过观察 SHOW ENGINE INNODB STATUS
的结果及参数innodb_adaptive_hash_index
来考虑是禁用或启动此特性,默认 AHI 为开启状态。
什么时候需要禁用呢?如果发现监视索引查找和维护哈希索引结构的额外 开销远远超过了自适应哈希索引带来的性能提升就需要关闭这个功能。
同时在 MySQL 5.7
中,自适应哈希索引搜索系统被分区。每个索引都绑定到 一个特定的分区,每个分区都由一个单独的 latch
锁保护。分区由 innodb_adaptive_hash_index_parts
配置选项控制 。在早期版本中,自适应哈 希索引搜索系统受到单个 latch
锁的保护,这可能成为繁重工作负载下的争用 点。innodb_adaptive_hash_index_parts
默认情况下,该 选项设置为 8
。最大 设置为 512
。当然禁用或启动此特性和调整分区个数这个应该是 DBA 的工作,作为开发了解即可。
全文检索之倒排索引
什么是全文检索(Full-Text Search
)?
它是将存储于数据库中的整本书或整 篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、 节、段、句、词等信息,也可以进行各种统计和分析。圈子中比较熟知的 Elasticsearch
、 Solr
等就是全文检索引擎,底层都是基于 Apache Lucene
的。
id | 朝代(dynasty) | 作者(author) | 诗词年代(poetry_age) | 标题(title) | 诗词全文(contents) |
---|---|---|---|---|---|
1 | 唐 | 李白 | 静夜思 | 床前明月光,疑是地上霜。 举头望明月,低头思故乡。 |
|
2 | 宋 | 李清照 | 如梦令 | 常记溪亭日暮,沉醉不知归路,兴尽晚回舟,误入藕花深处。争渡,争渡,惊起一滩鸥鹭。 | |
··· | ··· | ··· | ··· | ··· | ··· |
要根据朝代或者作者寻找诗,都很简单,比如select 诗词全文 from 诗词表 where 作者=‘李白’
,如果数据很多,查询速度很慢,怎么办?可以在对应的查询字段上建立索引加速查询。
但是如果现在有个需求:要求找到包含“望”字的诗词怎么办?
用 select 诗词全文 from 诗词表 where 诗词全文 like‘%望%’
,这个意味着 要扫描库中的诗词全文字段,逐条比对,找出所有包含关键词“望”字的记录。
基本上,数据库中一般的 SQL
优化手段都是用不上的。数量少,大概性能还能接 受,如果数据量稍微大点,就完全无法接受了,更何况在互联网这种海量数据的 情况下呢?怎么解决这个问题呢,用倒排索引。
比如现在有:
蜀道难(唐)李白 蜀道之难难于上青天,侧身西望长咨嗟。 |
都有望字,于是可以这么保存:
序号 | 关键字 | 蜀道难 | 静夜思 | 春台望 | 鹤冲天 |
---|---|---|---|---|---|
1 | 望 | 1 | 1 | 1 | 1 |
2 | 上 | 1 | 0 | 0 | 1 |
其实,上述诗词的中每个字都可以作为关键字,然后建立关键字和文档之间 的对应关系,也就是标识关键字被哪些文档包含。 | |||||
所以,倒排索引就是,将文档中包含的关键字全部提取处理,然后再将关键字和文档之间的对应关系保存起来,最后再对关键字本身做索引排序。用户在检索某一个关键字是,先对关键字的索引进行查找,再通过关键字与文档的对应关系找到所在文档。 | |||||
在存储在关系型数据库中的数据,需要事先分析将数据拆分为不同的字段,而在 es 这类的存储中,需要应用程序根据规则自动提取关键字,并形成对应关系。 |
索引在查询中的使用
索引在查询中的作用到底是什么?在的查询中发挥着什么样的作用 呢?
请记住:
**1、一个索引就是一个 B+树,索引让的查询可以快速定位和扫描到 需要的数据记录上,加快查询的速度。 **
2、一个 select 查询语句在执行过程中一般最多能使用一个二级索引,即 使在 where 条件中用了多个二级索引。
扫描区间
对于某个查询来说,最简单粗暴的执行方案就是扫描表中的所有记录,判断每一条记录是否符合搜索条件。如果符合,就将其发送到客户端,否则就跳过该 记录。这就是全表扫描。
对于使用 InnoDB
存储引擎的表来说,全表扫描意味着从聚簇索引第一个叶 子节点的第一条记录开始,沿着记录所在的单向链表向后扫描,直到最后一个叶 子节点的最后一条记录。虽然全表扫描是一种很笨的执行方案,但却是一种万能 的执行方案,所有的查询都可以使用这种方案来执行,只是效率不高。
有了索引,利用B+
树查找索引列值等于某个值的记录,这样可以明显减少需要扫描的记录数量。由于B+
树叶子节点中的记录是按照索引列值由小到 大的顺序排序的,所以即使只扫描某个区间或者某些区间中的记录也可以明显减 少需要扫描的记录数量。
比如下面这个查询语句:
SELECT * FROM order_exp WHERE id >= 3 AND id<= 99; |
这个语句其实是想查找id
值在[3,99]
区间中的所有聚簇索引记录。可以 通过聚簇索引对应的 B+
树快速地定位到 id
值为 3
的那条聚簇索引记录,然后沿着记录所在的单向链表向后扫描,直到某条聚簇索引记录的 id
值不在[3,99]
区间 中为止。
与全表扫描相比,扫描 id
值在[3,99]
区间中的记录已经很大程度地减少了需 要扫描的记录数量,所以提升了查询效率。其实所谓的全表扫描,可以理解 为扫描的区间是[负无穷,正无穷]
或者[第一条记录,最后一条记录]
。
范围区间扫描
其实对于 B+
树索引来说,只要索引列和常数使用=
、<=>
、IN
、NOT IN
、IS NULL
、 IS NOT NULL
、>
、<
、>=
、<=
、BETWEEN
、!=
(不等于也可以写成<>
)或者 LIKE
操作符连接起来,就可以产生一个区间。
1、IN
操作符的效果和若干个等值匹配操作符=
之间用OR
连接起来是一样 的,也就是说会产生多个单点区间,比如下边这两个语句的效果是一样的:
SELECT * FROM order_exp WHERE insert_time IN (2021-03-22 18:23:42, yyyy); |
2、!=
产生的扫描区间呢?
比如 SELECT * FROM order_exp WHERE order_no != 'DD00_9S'
此时使用 idx_expire_time
执行查询时对应的扫描区间就是[第一条记录 , 'DD00_9S']
和[ 'DD00_9S',最后一条记录]
。
3、LIKE
操作符比较特殊,只有在匹配完整的字符串或者匹配字符串前缀时 才产生合适的扫描区间。
所有搜索条件都可以使用某个索引的情况
有时候每个搜索条件都可以使用到某个索引,比如下边这个查询语句:
SELECT * FROM order_exp WHERE order_no > 'DD00_6S' AND order_no > 'DD00_9S'; |
这个查询中的搜索条件都可以使用到 idx_order_no
,也就是说每个搜索条件 都对应着一个 idx_order_no
的范围区间。这两个小的搜索条件使用 AND
连接起 来,也就是要取两个范围区间的交集,两者交集当然就是 order_no > 'DD00_9S'
了,也就是说上边这个查询使用 idx_order_no
的范围区间就是('DD00_9S', 最后 一条记录)
。
再看一下使用 OR
将多个搜索条件连接在一起的情况:
SELECT * FROM order_exp WHERE order_no > 'DD00_6S' OR order_no > 'DD00_9S'; |
OR
意味着需要取各个范围区间的并集,所以上边这个查询使用 idx_expire_time
的范围区间就是( 'DD00_6S' ,最后一条记录)
。
有的搜索条件无法使用索引的情况
比如下边这个查询:SELECT * FROM order_exp WHERE expire_time> '2021-03-22 18:35:09' AND order_note = 'abc';
请注意,这个查询语句中能利用的索引只有 idx_expire_time
一个,而 idx_expire_time
这个二级索引的记录中又不包含 order_note
这个字段,所以在使 用二级索引idx_expire_time
定位记录的阶段用不到 order_note = 'abc'
这个条件,这个条件是在回表获取了完整的用户记录后才使用的,而范围区间是为了到索引 中取记录中提出的概念,所以在确定范围区间的时候不需要考虑 order_note = 'abc'
这个条件。
使用联合索引执行查询时对应的扫描区间
联合索引的索引列包含多个列,B+树每一层页面以及每个页面中的记录采用 的排序规则较为复杂,以 order_exp 表的 u_idx_day_status 联合索引为例,它采 用的排序规则如下所示:
先按照 insert_time 列的值进行排序。
在 insert_time 列的值相同的情况下,再按照 order_status 列的值进行排序。
在 insert_time 和 order_status 列的值都相同的情况下,再按照 expire_time 列的值进行排序。
创建和删除索引的语句
-- 查看索引 |
索引的代价
空间上的代价
这个是显而易见的,每建立一个索引都要为它建立一棵 B+
树,每一棵 B+
树 的每一个节点都是一个数据页,一个页默认会占用 16KB
的存储空间,一棵很大 的 B+
树由许多数据页组成会占据很多的存储空间。
时间上的代价
每次对表中的数据进行增、删、改操作时,都需要去修改各个 B+
树索引。 而且B+
树每层节点都是按照索引列的值从小到大的顺序排序而组成了 双向链表。不论是叶子节点中的记录,还是非叶子内节点中的记录都是按照索引 列的值从小到大的顺序而形成了一个单向链表。
而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要 额外的时间进行一些记录移位,页面分裂、页面回收的操作来维护好节点和记录 的排序。如果建了许多索引,每个索引对应的 B+
树都要进行相关的维护操 作,这必然会对性能造成影响。
高性能索引创建策略
索引列的类型尽量小
在定义表结构的时候要显式的指定列的类型,以整数类型为例,有 TTNYINT
、NEDUMNT
、INT
、BIGTNT
这么几种,它们占用的存储空间依次递增, 这里所说的类型大小指的就是该类型表示的数据范围的大小。能表示的整数 范围当然也是依次递增,如果想要对某个整数列建立索引的话,在表示的整 数范围允许的情况下,尽量让索引列使用较小的类型,比如能使用 INT
就不 要使用 BIGINT
,能使用 NEDIUMINT
就不要使用INT
,这是因为:
- 数据类型越小,在查询时进行的比较操作越快(
CPU
层次) - 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下 更多的记录,从而减少磁盘
I/O
带来的性能损耗,也就意味着可以把更多的数据 页缓存在内存中,从而加快读写效率。
这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值, 其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键适用更小的 数据类型,也就意味着节省更多的存储空间和更高效的 I/O
。
索引选择性和前缀索引
创建索引应该选择选择性/离散性高的列。索引的选择性/离散性是指,不重复的索引值(也称为基数,cardinality
)和数据表的记录总数(N
)的比值,范围从 1/N
到 1
之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让 MySQL
在查找时过滤掉更多的行。唯一索引的选择性是 1
,这是最好的索引选择性,性能也是最好的。
很差的索引选择性就是列中的数据重复度很高,比如性别字段,不考虑政治正确的情况下,只有两者可能,男或女。那么在查询时,即使使用这个索引, 从概率的角度来说,依然可能查出一半的数据出来。
哪列做为索引字段最好?当然是姓名字段,因为里面的数据没有任何重复, 性别字段是最不适合做索引的,因为数据的重复度非常高。
怎么算索引的选择性/离散性?比如 order_exp
这个表:
select COUNT(DISTINCT order_no)/count(*) cnt from order_exp; |
![image-20210512172539977](/medias/blog/image-20210512172539977.png)
有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是前面提 到过的模拟哈希索引。
模拟哈希索引:
order_exp
表中 order_note
字段很长,想把它作为一个索引,可以增加 一个 order_not_hash 字段来存储 order_note
的哈希值,然后在 order_not_hash
上 建立索引,相对于之前的索引速度会有明显提升,一个是对完整的 order_note
做索引,而后者则是用整数哈希值做索引,显然数字的比较比字符串的匹配要高 效得多。
但是缺陷也很明显:
1、需要额外维护 order_not_hash
字段;
2、哈希算法的选择决定了哈希冲突的概率,不良的哈希算法会导致重复值 很多;
3、不支持范围查找。
还可以做些什么改进呢?还可以索引开始的部分字符,这样可以大大节约索 引空间,从而提高索引效率。但这样也会降低索引的选择性。一般情况下需 要保证某个列前缀的选择性也是足够高的,以满足查询性能。(尤其对于 BLOB
、 TEXT
或者很长的 VARCHAR
类型的列,应该使用前缀索引,因为 MySQL
不允许索 引这些列的完整长度)。
诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便 节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换 句话说,前缀的“基数”应该接近于完整列的“基数”。
为了决定前缀的合适长度,可以找到最常见的值的列表,然后和最常见的前 缀列表进行比较。
只为用于搜索、排序或分组的列创建索引
只为出现在 WHERE
子句中的列、连接子句中的连接列创建索引, 而出现在查询列表中的列一般就没必要建立索引了,除非是需要使用覆盖索引; 又或者为出现在 ORDER BY
或 GROUP BY
子句中的列创建索引
多列索引
一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。
遇到的最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使 用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。在一个多列 B-Tree
索引中,索引列的顺序意味着索引首先按照最左列进 行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的 ORDER BY
、GROUP BY
和 DISTINCT
等子句的查询需求。
所以多列索引的列顺序至关重要。对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引最前列。当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化 WHERE
条件 的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在 WHERE
子句中只使用了索引部分前缀列的查询来说选择性也更高。
然而,性能不只是依赖于索引列的选择性,也和查询条件的有关。可能需要 根据那些运行频率最高的查询来调整索引列的顺序,比如排序和分组,让这种情 况下索引的选择性最高。
同时,在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足 不同类型的查询需求。
设计三星索引
啥是三星索引?
对于一个查询而言,一个三星索引,可能是其最好的索引。
如果查询使用三星索引,一次查询通常只需要进行一次磁盘随机读以及一次窄索引片的扫描,因此其相应时间通常比使用一个普通索引的响应时间少几个数量级。
三星索引概念是在《Rrelational Database Index Design and the optimizers》
一 书中提出来的。
达成三星索引
现在有表
create table customer( cno int, lname varchar(10), fname varchar(10), sex int, weight int, city varchar(10)); |
对于下面的 SQL 而言,这是个三星索引
select cno,fname from customer where lname =’xx’ and city =’yy’ order by fname; |
来评估下:
第一颗星:所有等值谓词的列,是组合索引的开头的列,可以把索引片缩得 很窄,符合。
第二颗星:order by
的 fname
字段在组合索引中且是索引自动排序好的,符合。
第三颗星:select
中的 cno
字段、fname
字段在组合索引中存在,符合。
达不成三星索引
现在有表
CREATE TABLE `test` |
SQL 语句如下:
select user_name,sex,age from test where user_name like 'test%' and sex =1 ORDER BY age |
如果建立索引(user_name,sex,age)
:
第三颗星,满足
第一颗星,满足
第二颗星,不满足,user_name
采用了范围匹配,sex
是过滤列,此时 age
列 无法保证有序的。
上述看到,此时索引(user_name,sex,age)
并不能满足三星索引中的第二颗星(排序)。
于是改改,建立索引(sex, age,user_name)
:
第一颗星,不满足,只可以匹配到 sex
,sex
选择性很差,意味着是一个宽索引片。
第二颗星,满足,等值 sex
的情况下,age
是有序的。
第三颗星,满足,select
查询的列都在索引列中。
对于索引(sex,age,user_name)
可以看到,此时无法满足第一颗星,窄索引片的需求。
以上 2 个索引,都是无法同时满足三星索引设计中的三个需求的,只能尽力满足 2 个。而在多数情况下,能够满足 2 颗星,已经能缩小很大的查询范围 了,具体最终要保留那一颗星(排序星 or
窄索引片星),这个就需要看查询者 自己的着重点了,无法给出标准答案。
主键是很少改变的列
行是按照聚集索引物理排序的,如果主键频繁改变(update
),物理顺序会改变,MySQL
要不断调整 B+
树,并且中间可能会产生页面的分裂和合并等等,会导致性能会急剧降低。
冗余和重复索引
MySQL
允许在相同列上创建多个索引,无论是有意的还是无意的。MySQL
需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑, 这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索 引。应该避免这样创建重复索引,发现以后也应该立即移除。
删除未使用的索引
除了冗余索引和重复索引,可能还会有一些服务器永远不用的索引。这样的 索引完全是累赘,建议考虑删除。
高性能索引使用策略
先总结一下,索引的使用策略一共有13中。
- 1、 尽量全值匹配
- 2、最佳左前缀法则
- 3、 覆盖索引尽量用
- 4、不等于要慎用
- 5、Null/Not 有影响
- 6、Like 查询要当心
- 7、字符类型加引号
- 8、使用索引扫描来做排序和分组
- 9、排序要当心
- 10、尽可能按主键顺序插入行
- 11、优化 Count 查询
- 12、优化 limit 分页
附上别人的记忆总结:下面针对每一种进行分析,测试:全职匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
LIKE 百分写最右,覆盖索引不写*;
不等空值还有 OR,索引影响要注意;
VAR 引号不可丢, SQL 优化有诀窍。不在索引列上做任何操作
通常会看到一些查询不当地使用索引,或者使得MySQL
无法使用已有 的索引。如果查询中的列不是独立的,则MySQL
就不会使用索引。“独立的列” 是指索引列不能是表达式的一部分,也不能是函数的参数。
例如,假设order_status
上有索引,但是下面这个查询无法使用order_status
列的索引:凭肉眼很容易看出SELECT * FROM order_exp WHERE order_status + 1 = 1;
WHERE
中的表达式其实等价于order_status = 0
,但是MySQL
无法自动解析这个方程式。这完全是用户行为。应该养成简化 WHERE 条件的习惯,始终将索引列单独放在比较符号的一侧。
下面是另一个常见的错误:在索引列上使用函数,也是无法利用索引的。SELECT ... WHERE TO_DAYS(insert_time) - TO_DAYS(expire_time) <= 10;
尽量全值匹配
建立了联合索引列后,如果我们的搜索条件中的列和索引列一致的话,这种 情况就称为全值匹配,比方说下边这个查找语句:我们建立的select * from order_exp where insert_time='2021-03-22 18:34:55' and order_status=0 and expire_time='2021-03-22 18:35:14';
u_idx_day_statusr
索引包含的3个列在这个查询语句中都展现出 来了,联合索引中的三个列都可能被用到。
也许有个疑问,WHERE 子句中的几个搜索条件的顺序对查询结果 有啥影响么?也就是说如果我们调换insert_time
,order_status
,expire_time
这 几个搜索列的顺序对查询的执行过程有影响么?比方说写成下边这样:放心,Select * from order_exp where order_status=1 and expire_time='2021-03-22 18:35:14' and insert_time='2021-03-22 18:34:55';
MySQL
没这么蠢,查询优化器会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定先使用哪个搜索条件,后使用哪个搜索条件。
所以,当建立了联合索引列后,能在where
条件中使用索引的尽量使用。最佳左前缀法则
建立了联合索引列,如果搜索条件不够全值匹配怎么办?在我们的搜索语句 中也可以不用包含全部联合索引中的列,但要遵守最左前缀法则。指的是查询从 索引的最左前列开始并且不跳过索引中的列。
比如:或select * from order_exp where insert_time='2021-03-22 18:23:42' and order_status=1;
那为什么搜索条件中必须出现左边的列才可以使用到这个 B+树索引呢?比如下边的语句就用不到这个select * from order_exp where insert_time='2021-03-22 18:23:42' ;
B+
树索引么?或SELECT * FROM order_exp WHERE order_status=1;
Select * from order_exp where order_status=1 and expire_time='2021-03-22 18:35:14';
explain
查询了执行计划后,发现是全表扫描。
想一下,因为B+
树的数据页和记录先是按照insert_time
列的值排序的, 在insert_time
列的值相同的情况下才使用order_status
列进行排序,也就是说insert_time
列的值不同的记录中order_status
的值可能是无序的。而现在你跳过insert_time
列直接根据order_status
的值去查找,怎么可能呢?expire_time
也是 一样的道理,那如果我就想在只使用expire_time
的值去通过B+
树索引进行查找 咋办呢?这好办,你再对expire_time
列建一个B+
树索引就行了。
但是需要特别注意的一点是,如果我们想使用联合索引中尽可能多的列,搜 索条件中的各个列必须是联合索引中从最左边连续的列。比方说联合索引u_idx_day_status
中列的定义顺序是insert_time
,order_status
,expire_time
,如 果我们的搜索条件中只有insert_time
和expire_time
,而没有中间的order_status
,只能用到select * from order_exp where insert_time='2021-03-22 18:23:42' and expire_time='2021-03-22 18:35:14';
insert_time
列的索引,order_status
和expire_time
的索引就用不上 了,道理不用多说了。范围条件放最后
这一点,也是针对联合索引来说的,前面我们反复强调过,所有记录都是按 照索引列的值从小到大的顺序排好序的,而联合索引则是按创建索引时的顺序进 行分组排序。
比如:由于select * from order_exp_cut where insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00';
B+
树中的数据页和记录是先按insert_time
列排序的,所以我们上边的 查询过程其实是这样的:
找到insert_time
值为2021-03-22 18:23:42
的记录。
找到insert_time
值为2021-03-22 18:35:00
的记录。
由于所有记录都是由链表连起来的,所以他们之间的记录都可以很容易的取 出来,找到这些记录的主键值,再到聚簇索引中回表查找完整的记录。
但是如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行 范围查找的时候才能用到B+
树索引:select * from order_exp_cut where insert_time>'2021-03-22 18:23:42' and insert_time<'2021-03-22 18:35:00' and order_status > -1;
覆盖索引尽量用
不等于要慎用
Null/Not 有影响
Like 查询要当心
字符类型加引号
使用索引扫描来做排序和分组
排序要当心
尽可能按主键顺序插入行
优化 Count 查询
优化 limit 分页
2021-05-13-MySQL 索引总结