Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > Mysql高性能索引

Mysql中创建高性能索引详解

作者:阿柠xn

这篇文章主要介绍了Mysql中创建高性能索引详解,索引相信大家都听说过,但是真正会用的又有几人,平时工作中写SQL真的会考虑到这条SQL如何能够用上索引,如何能够提升执行效率,文本就来详细解读如何创建高性能索引,需要的朋友可以参考下

创建高性能索引

索引优化应该是对查询性能优化的最有效的手段了。索引可以轻易的将查询性能提升几个数量级。

索引基础

SELECT first_name FROM 表 WHERE id=5;

对于上面这个查询,如果id列上有索引。则MySQL将使用该索引找到id=5的行,也就是说,mysql现在索引上按值查找,然后返回所有包含该值的数据行。

索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效的使用索引的最左前缀列。

索引的类型

在MySQL中,索引是在存储引擎层而不是服务器层实现的。

下面介绍mysql支持的索引类型:

B-Tree索引

当我们在谈论索引的时候,如果没有特别的指明类型,那么多半都是B-Tree索引,他使用B树数据结构来存储数据。当然更多的使用的是B+树的数据结构,你比如说Innodb。

image-20220915100309784

B树索引为什么能够加快数据的访问速度呢,这就打到我的甜点位了,因为存储引擎不需要进行全表扫描了呀,取而代之的是从索引的根节点开始进行搜索,并且每个子节点都有子节点页的上限和下限,最终引擎要么就是找到,要么就是找不到。

还有一个点B+树是所有节点信息都在叶子节点保存着呢

请注意:索引对多个值进行排序的依据是你在建表语句中定义索引时列的顺序。

你比如说如下的数据表:

CREATE TABLE People(
	last_name varchar(50) not null,
	first_name varchar(50) not null,
	dob date  not null,
	gender enum('m','f') not null,
	key(last_name,frist_name,dob)
);

咱就是说,这个key,我又想提几句

MySQL中有四种Key: Primary Key, Unique Key, Key 和 Foreign Key。

除了Foreign Key最好理解外,其他的都要区分一下。

剩下的三种都要在原表上建立索引。

Primary Key和Unique Key之间的区别网上说的最多。Primary Key的提出就是为了唯一标示表中的字段,就像我们的身份证号一样。此外,所有字段都必须是not null的Unique Key则是为了保证表中有些字段是唯一的。比如有些单位领导叫“张三”,所以下面招人的时候是决不可招一个有同样名字的。

至于Key吗,网上说的比较少。其实某个字段标记为Key,是不能保证这个字段的值在表中是唯一出现的。它的目的就是建立索引。

之前所述的索引对下面的类型的查询都是有效的:

因为呀,这个索引树中的节点是有序的,所以处理按值查找外,索引还可以用于查询中的order by 操作,一般来说,

如果B树可以按照某种方式查找到值,那么也可以按照这种方式用于排序,所以,如果orderby 子句满足我们刚刚上面提到的查询类型,那么这个索引也可以满足对应的排序需求。

下面呢,我就要你介绍一些B树索引的限制了:

WHERE last_name = 'Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23'

这个查询就只能使用索引的前两列,因为like是一个范围条件。

所以,如果范围查询列值的数量有限,那么就可以通过使用多个等于条件来代替范围条件。

哈希索引

哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码。哈希码是一个比较小的值,并且不同键值的行计算出来的哈希码也不一样,哈希索引将所有的哈希码存储在索引中,同时哈希表中保存指向每个数据行的指针。

在MySQL中只有memory引擎显示支持哈希索引。这也是memory引擎表的默认索引类型。

select name FROM 表  WHERE name=‘peter';

哈希索引查询过程:

MySQL先计算’peter’的哈希值,并使用该值寻找对应的记录指针。f(peter)=8784 。所以mysql在索引中查找8784。就可以找到一个指向表的第三行的指针,最后一步,比较第三行的值是不是peter,以确保就是要查找的行。

因为索引只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度十分快。

但是

哈希索引,也是有他自己的限制。

创建自定义哈希索引

如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这样同样可以使用一些哈希索引的便利。

思路:

在b树的基础上创建一个伪哈希索引,这和真正的哈希索引不是一回事,因为还是使用b树进行查找,但是他使用的是哈希值不是键本身进行索引查找,你需要做的是在查询的WHERE子句中手动指定使用哈希函数。

比如:

我们下面这个查询url的语句:

select id FROM url where url='www.baidu.com';

如果我们删除原来url列上的索引,而新增一个url_crc列,使用crc32做哈希,就如下面:

select id FROM url where url='www.baidu.com'
AND url_crc=CRC32('www.baidu.com');

你这样做,他的性能就很高,有多高,有三四层楼那么高。

因为MySQL优化器会使用这个选择性很高而体积很小的基于url_crc列的索引来完成查找。即使多个记录有相同的索引值,查找依旧很快,只需根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回的对应行。

当然这种实现的缺陷是需要维护哈希值。可以手动维护,也可以使用触发器实现。

如果使用这种方式,你也切记不要使用sha1()和md5()作为哈希函数因为这两函数计算出来的哈希值是特别长的字符串,会浪费大量空间,比较时也会比较慢。

处理哈希冲突

就像上面那个哈希索引查询,你是必须包含常量值的在WHERE子句中,不然哈希值冲突你就没法搞了呀:

select id FROM url where url='www.baidu.com'
AND url_crc=CRC32('www.baidu.com');

空间索引

myisam表支持空间索引,可以用作地理数据存储。和b树索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效的使用任意维度来组合查询。必须使用MySQL的GIS相关函数如mbrcontains()等来维护数据。

全文索引

全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引的匹配方式完全不一样。他有许多需要注意的细节,如停用词,词干和复数,布尔搜索等。

其他索引类别

还有许多第三方存储引擎使用不同类型的数据结构来存储索引。例如TokuDB使用分形树索引。这是一类较新开发的数据结构,既有b树的很多优点,也避免了b树的一些缺点。

索引的优点

索引可以让服务器快速的定位到表的指定位置,但是这并不是索引的唯一作用。

最常见的b树索引,按照顺序存储数据,所以mysql可以用来做order by和group by 操作。因为数据是有序的,所以b树也会将相关的列值存储在一起。最后 ,因为索引中存储了实际的列值,所以某些查询只使用索引就能完成全部查询:

总而言之,言而总之,有着如下的优点:

索引大大减少了服务器需要扫描的数据量索引可以帮助服务器避免排序和临时表索引可以将随机IO变为顺序IO。

思考一个问题,索引时最好的解决方案吗?

其实不见的,对于非常小的表,大多是扫描全表是更加高效的,对于中到大型表索引就很有效,对于特大型表,建立和使用索引的代价将随之增长,这种情况下就要使用分区技术了,这个我们后面的文章介绍。

高性能的索引策略

独立的列

如果查询的列不是独立的,则MySQL就不会使用索引。独立的列是指索引列不能是表达式的一部分,也不能是函数的参数

如:

select actor_id FROM sakila.actor where actor_id + 1 = 5;

其实屏肉眼你是可以判断出这个actor_id其实就是=4,但是mysql是无法自动解析这个方程式的。这完全是用户行为,我们应该养成习惯,始终将索引列单独放在比较符号的一侧

前缀索引和索引的选择性

有时啊,需要的索引很长的字符列,这样呢会使索引变得大且慢。一个策略是前面提到过的模拟哈希索引。再者呢?

我们可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。

但是

这样会降低索引的选择性:

索引的选择性=不重复的索引值和数据表的记录总数的比值。

索引的选择性越高则查询的效率越高,因为选择性高的会在查找时过滤掉更多的行。

当然对于blob,text,varchar这种很长类型的列,必须使用前缀索引,mysql不允许索引这些列的完整长度。

多列索引

很多人对多列索引的理解不够。一个常见的错误就是,为每个列都创建独立的索引,或者按照错误错误的顺序创建多列索引。

你比如说下面这个例子:

CREATE TABLE t(
c1 int,
c2 int,
c3 int,
key(c1),
key(c2),
key(c3)
);

像这样在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。

但是

MySQL5.0和更新的版本引入了一个叫“索引合并”的策略,一定程度上是可以使用表上的多个单列索引来定位指定的行。在此之前的版本的MySQL只能使用其中某个单列索引。

你再比如说下面这个例子:

select film_id,actor_id from biao
where actor_id =1 OR film_id = 1;

在老版本的mysql中,你像上面这样where里的OR语句的情况是不能使用两个单列索引进行扫描的,只会进行全表扫描。除非你把or改成UNION的形式。

索引合并策略确确实实的解决了上面我们所描述的那种情况。但实际上更多时候还是说明呀,表上的索引建的hin糟糕:

所以:如果在explain中看到有索引合并,应该好好去检查一下查询的表和结构,看看是不是已经最优了。你可以通过参数optimizer switch 来关闭索引合并功能。也可以使用ignore index 提示优化器忽略掉某些索引。

选择合适的索引顺序

我们遇到的最容易引起困惑的问题就是索引列的顺序。这个顺序的正确性其实是依赖使用该索引的查询是怎么样的。并且我们还需要考虑如何更好的满足排序和分组的需要。(顺带说一嘴,本节内容适用于b树索引)

我们之前也有介绍,在一个多列b树索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,索引可以按照升序或者是降序进行扫描,以满足查询列里的orderby groupby 和distinct等子句的查询需求。

多列索引的列顺序是至关重要的。

那么,对于如何选择索引的列顺序呢?有这样一个经验法则:将选择性最高的列放到索引的最前列。当然了场景不同,选择不同,没有那种放任四海皆准的法则。

当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用就是用于优化where条件的查找。在这种情况下,这样设计的索引确实能够最快的过滤出需要的行,对于在where子句中只使用索引部分前缀列的查询来说选择性也更高。当然为了性能你还得考虑哪些列是运行频率比较高的,来调整索引列的顺序,让这种情况下的索引的选择性最高。

你比如说下面这个例子:

SELECT * FROM 表 
WHERE
staff_id = 2 AND customer_id = 584

那么你就要考虑是创建一个(staff_id,customer_id)索引,还是两者调换一下顺序呢?

这个我们就得去查询一下表中值的分布情况,来确定哪个列的选择性更高。

我们一查就发现:

staff_id有7992个数据,staff_id只有30个数据。

那么根据我们的经验法则,我们应该将索引列customer_id放到前面,因为对应条件值的customer_id数量更少。

这样做其实你还要注意一个地方,因为这个查询结果是十分依赖选定的具体值。如果按照上面的查询结果进行优化,可能对其他的值不公平,进而导致服务器整体的性能可能更糟。

但是一般情况下我们就用下面介绍的经验法则就可以知道那个字段选择性更高:

SELECT COUNT(DISTINCT staff_id)/count(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM 表;

这样我们可以看出来customer_id的选择性更高, 所以就将它作为索引的第一列

ALTER TABLE 表  payment ADD KEY (customer_id,staff_id);

聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,比如说innodb的聚簇索引实际上在同一个结构中保存了b树索引和数据行。

当表中有聚簇索引时,他的数据行实际上存放在索引的叶子页中。

如果没有定义主键,innodb会选择一个唯一的非空索引代替。如果没有这样的索引,innodb会隐式定义一个主键来作为聚簇索引。innodb只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。

但是聚簇主键可能对性能有帮助,也可能导致严重的性能问题

聚集的数据有一些重要的优点:

聚簇索引的一些缺点:

最后一点,做出一个解释,为什么二级索引需要两次索引查找?因为二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。这就意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值。然后根据这个值去聚簇索引中查找到对应的行。

覆盖索引

通常啊,我们大家都会根据查询的where条件来创建合适的索引,不过这只是索引优化的一个方面。设计优秀的索引是应该考虑到整个查询,而不是单单where条件部分。索引确实是一种查找数据的高效方式,但是mysql也可以使用索引来直接获取列数据,这样就不需要再读取数据行。

咱就是说,如果索引的叶子节点中已经包含要查询的数据,那么还有什么必要回表查询呢?

如果一个索引包含所有需要查询的字段的值,我们就称之为“覆盖索引”。

如果咱们查询只是需要扫描索引而无须回表,是会带来很多好处的:

当然,我们要清楚一点,不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,而哈希索引,空间索引和全文索引等都不存储索引列的值,所以mysql只能使用b树索引做覆盖索引。

但是

索引覆盖查询还是有很多陷阱可能导致无法实现优化,MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖,假设索引覆盖了where条件中的字段,但不是整个查询涉及的字段。MySQL5.5或者更早的版本还是会回表获取数据行,尽管这一行可能最终还是会被过滤掉。

我们举个栗子:

SELECT * FROM products where actor = 'SEAM CARREY'
AND title like '%APOLLO%';

像上面这个查询索引是无法覆盖的,有两个原因:

也有办法是可以解决我上面说的这两个问题的,那就是需要重写查询并巧妙地设计索引。首先将索引扩展到覆盖是三个数据列(actor,title,prod_id),然后按照如下的方式重写查询。

select * 
from products
join (
	select prod_id
	from products
	where actor = 'SEAM CARREY' AND title like '%APOLLO%';
) AS t1 ON (t1.prod_id=products.prod_id);

我们把这种方式叫做延迟关联,因为延迟了对列的访问。在查询的第一阶段MySQL可以使用覆盖索引,在from子句的子查询中找到匹配的prod_id,然后根据这些prod_id 值在外层查询匹配获取需要的所有列值,虽然无法使用索引覆盖整个查询,但总算比完全无法利用索引覆盖的好。

延时关联,即通过使用覆盖索引查询返回需要的主键,再根据主键关联原表获得需要的数据,尤其在大分页查询的场景下,可以提高查询效率。

使用索引扫描来做排序

MySQL有两种方式可以生成有序的结果:通过排序操作;或者按照索引顺序扫描;如果explain出来的type列的值是index,则说明mysql使用了索引扫描来做排序(不要和extra列的using index高混淆了)

扫描索引本身是很快的,因为只需要从一条索引记录移动到紧挨着的下一条记录。但是如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机IO,因此按索引顺序读取数据的速度通常要比顺序的全表扫描慢,尤其是在IO密集型的工作负载时。

只有当索引的列顺序和order by子句顺序完全一致,并且所有列的排序方向都一样时,MySQL才能用索引来对结果做排序。如果查询需要关联多张表,则只有当order by子句引用的字段全为第一个表时,才能使用索引做排序。order by子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则MySQL都需要执行排序操作,而无法利用索引排序。

当然有一种情况下order by子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。如果where子句或者join子句中对这些列指定了常量,就可以弥补索引的不足。

比如说我们举一个例子:

对于下面这样一个表结构:

create table 表名(
。。。。
key lizi (date,id1,id2),
。。。。
)

然后对于下面这个查询语句:

select 内容
from  表
where date  = 11
order by id1,id2;

这种查询,MySQL就可以使用lizi这个索引为查询排序。当然,我们可以看到即使order by这个子句是不满足最左前缀的要求的,但是没关系呀,索引的第一列date的被指定为了一个常数呀。

你再比如像下面这样:

where date = 常数
order by id1;

你像上面这个还是可以使用索引排序的,第一列常数,然后再索引的最左前缀嘛。

你再在比如说下面这个:

where date >日期 order by  date,id1;

这个就也能用索引排序,order by使用的两列就是索引的最左前缀。

都讲了这么老半天的能用索引排序的,那么下面我就来说说这个不能使用索引排序的查询。

where date = 常数 order by id1 desc ,id2  asc;
where  date  = 常数 order by id1 ,id3;
where date = 常数 order by id2;
where date > 常数 order by id1,id2;
where date = 常数 AND id1 IN (1,2)order by id2;

压缩(前缀压缩)索引

myisam使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大的提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。

myisam压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即可。

就是这样,你比如说:索引块的第一个值是“perform”,第二个值是“performance”,那么第二个值的前缀压缩后存储的类似“7,ance”这样的形式。myisam对行指针也采用类似的前缀压缩方式。

压缩块使用更少的空间,代价是某些操作可能更慢。这你也是可以想象的嘛,因为前缀压缩了,你必然是不能二分查找,只能顺序查找 了,对于order by desc 这种倒序的,那就更难受了。

我们可以在create table 语句中指定pack_keys参数来控制索引压缩的方式。

冗余和重复索引

MySQL是允许在相同列上创建多个索引,无论你创建的时候是有意的还是无意的。MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要进行逐个考虑,这回影响性能。

重复索引,啥叫重复索引,就是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样的创建,发现之后也应该立即移除。

你比如说下面这样的例子:

create table test(
		ID int NOT null primary key,
		......
		unique(ID),
		INDEX(ID)
)

这上面,你创建了一个主键,先加上唯一限制,然后再加上索引以供查询使用。事实上呀,MySQL的唯一限制和主键限制都是通过索引实现的 ,因此呀,上面这个写法其实就是在相同的列上 创建了三个重复的索引。通常情况下是没有必要这样做的,除非呀是在同一列上创建不同类型的索引来满足不同的查询需求。

冗余索引和重复索引其实又有一些不同。如果创建了索引(A,B),再创建索引(A)那就是冗余索引了,因为这只是前一个索引的前缀索引。但是你要是创建的是(B,A)那可就不是冗余索引了。当然不同的索引类型肯定也不会涉及到冗余索引的事情。

还有一种情况,将索引(A)扩展为了索引(A,ID),其中ID是主键,这在innodb中来说,主键列已经包含到二级索引中去了,所以这也是冗余索引。

大多数情况下,我们不应该是创建新索引,而是扩展已有的索引。但你话也不能说死,有时候出于性能的考虑,也会考虑冗余索引,因为你扩展索引会导致其变得太大,从而影响其他使用该索引的查询的性能。

未使用的索引

除了冗余索引,和重复索引,还有些索引呀,服务器可能这辈子都用不到,这种也要考虑删除掉呀。

索引和锁

索引可以让查询锁定更少的行。

总结

在选择索引和编写利用索引的查询时:有三个原则需要始终牢记。

到此这篇关于Mysql中创建高性能索引详解的文章就介绍到这了,更多相关Mysql高性能索引内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文