MySQL索引那些事儿

2019-06-16  本文已影响0人  AlgoPeek

1.写在前面

索引对于良好的性能非常关键,特别是当表中的数据量越来越大的时候,索引对性能的影响愈发重要。而索引优化应该是对查询优化最有效的手段了,索引能够轻易将查询性能提高几个数量级。本文是对MySQL索引的一个总结,希望通过本文能够回答以下问题:

2.索引基础

按照MySQL官方的定义:索引(在MySQL中也叫键(key))是存在引擎用于快速找到记录的一种数据结构。

从索引的定义可以知道,索引其本质是一种数据结构。索引有多种类型,如B-Tree索引、哈希索引、全文索引、空间数据索引(R-Tree)等。对于MySQL,索引是在存储引擎层实现,如果没有特别说明,我们说的索引都是使用B-Tree索引,从技术实现上来讲,MySQL B-Tree索引是其变种B+Tree实现的,下图大致反映InnoDB索引是如何工作的,MyISAM使用的结构有所不同,但基本思想类似,后边会详细说:


简单来说,对于非叶子结点:除了保存键值(key)外,还保存了指定子页结点的指针,且key左边指针指向的结点的值均小于key,key右边指针指向的值均大于等于key;对于叶子结点,结点中的每一个key包含了一个指向数据的指针(依赖于不同存储引擎,InnoDB存储了原始数据,MyISAM存储了被索引行的物理位置)并且每个结点包含一个指向下一个叶子页的指针。

为什么说B-Tree索引能够加快访问数据的速度呢?因为存储引擎不需要再进行全表扫描来获取需要的数据,取而代之的是从索引根结点开始进行二分查找,如果找到则返回对应节点的值,否则对相应槽中的指针指向的页结点递归进行查找,直到找到节点或记录不存在。

3.索引的优缺点

3.1优点

3.2缺点

4. B-Tree索引的查询类型

通过上节已经知道了B-Tree索引的数据结构,下面通过例子看看为什么B-Tree索引的顺序至关重要,假如有如下数据表:

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, first_name, dob)
)ENGINE=InnoDB;

对于每一行数据,索引中包含 last_name, first_namedob列的值,下图展示了索引是如何组织数据存储的:

需要注意的是,索引对于多个值进行排序的依赖是CREATE TABLE语句中定义索引时列的顺序。如最后两个人的姓我名都一样,则根据他们的出生日期进行排序。

B-Tree索引适合于全键值、键值范围或键前缀查找。其中键前缀查找只适用于最左前缀查找,上例索引对如下类型的查询有效:

如果不按索引的最左列开始查找,则无法使用索引。例如上面例子中如果我们查找名字为Bill的人或查找某个特定生日的人,则无法使用索引,存储引擎会进行会表扫描,这便是为什么索引列的顺序会影响到查询的原因。

5. 高性能索引策略

正确地创建和使用索引是实现高性能查询的基础。通过下面这些索引策略可以真正发挥索引的优势。

5.1 前缀索引和索引选择性

有时候需要索引很长的字符串,这会让索引变得大且慢,根据前面讲过的最左前缀,通过可以索引字符串开始的部分字符,这样可以大大节约索引存储空间,提高索引效率。但这会降低索引的选择性

索引的选择性是指不重复的索引值,也称为基数(cardinality)和数据表的记录总数(#T)的比值,范围从1/#T到1之间,索引的选择性越高则查询效率越高。我们通常可以采用以下公式进行选择性计算:

Selectivity = COUNT(DISTINCT(field)) / COUNT(*)

来看一个例子,例子来源MySQL employee example

CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` enum('M','F') NOT NULL,
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`),
) ENGINE=InnoDB;

如上表,出于业务的需求,我们需要根据first_namelast_name进行查找,可以为<first_name,last_name>建立索引,但fist_namelast_name加起来长度为30,可以考虑用first_namelast_name的前几个字符建立索引。但last_name取几个字符呢,我们可以通过计算其选择性来决定,比如<first_name, left(last_name, 3)>,其选择性如下:

mysql> SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.7879 |
+-------------+

看起来还不错,如果把last_name的前缀加到4,看看选择性如下:

mysql> SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9007 |
+-------------+

这个选择性已经比较好了,而且索引长度也只有18,比起<first_name, last_name>短了近一半,于是可以建立索引:

ALTER TABLE employees.employees
ADD INDEX `first_name_last_name4` (first_name, last_name(4));

5.2 多列索引

多列索引也叫联合索引。很多人对多列索引理解不够,一个常见的错误就是为每一个列创建独立的索引,或者按照错误的顺序创建多列索引。这种理解是非常错误的,在多个列上建立独立的索引大部分情况下并不能提高MySQL的查询性能。

当出现服务器对多个索引做相交操作(通常是多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。

5.3 选择合适的索引列顺序

在一个多列的B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。索引是按照升序或降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY、和DISTINCT等子句的查询需求。所以多列索引的列顺序至关重要。

5.4 聚簇索引和非聚簇索引

聚簇索引并不是一种单独的索引类型,而一种数据存储方式。

InnoDB采用的是聚簇索引,其实现原理是在同一个结构中保存了B-Tree索引和数据行,如下图是聚簇索引的分布图,索引列是包含的整数值,叶子页中包含了行的全部数据:

对于聚簇索引,插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。另外基于聚簇索引表在插入新行,或主键被更新导致需要移动行的时候,可能面临“页分裂”的问题,如果主键采用递增方式顺序插入,也能避免页分裂造成随机I/O和占用更多磁盘空间的问题。

对于InnoDB引擎是通过主键聚集数据的,也就是说上图中的被索引的列就是主键。如果没有定义主键,InnoDB会选择一个惟一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。

对于InnoDB引擎,二级索引的叶子节点保存的不是指向行的物理位置,而是行的主键值,也就是说二级索引访问需要两次索引查找,而不是一次

相对于聚簇索引,非聚簇索引是指索引和数据是分离的。MyISAM存储引擎是采用的非聚簇索引。其索引的叶结点存放的不再是数据,而是存储的数据的物理地址,为了简单说明,可以理解为“行号”,如下图所示:

下图可以很容易看出InnoDB(聚簇索引)和MyISAM(非聚簇索引)保存数据和索引的区别:


5.5 覆盖索引

如果一个索引包含(或者说覆盖)所有需要查询的字段值 ,我们就称之为覆盖索引。覆盖索引能极大提高查询性能,因为只需要扫描索引而无须回表,有如下好处:

5.6 使用索引扫描做排序

MySQL有两种方式可以生成有序结果:

如何判断使用发哪些方式呢?可以使用explain查看type列的值(这里强烈建议看看MySQL帮忙手册中对explain中各个字段的解释,这对优化查询的时候至关重要),如果是index则表示MySQL使用了索引扫描来排序。

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

5.7 冗余和重复索引

MySQL允许在相同的列上创建多个索引。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。

注意重复索引不是冗余索引。如果创建的索引<A, B>,再创建索引<A>就是冗余索引,因为索引<A>只是索引<A,B>的前缀索引。因此<A,B>可以当作索引<A>来使用。但如果再创建索引<B,A>,则不是冗余索引,因为<B>不是<A,B>的最左前缀。

大多数情况下都不需要冗余索引,但有时候出于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询性能。

总结

选择索引和编写利用这些索引的查询时,有三个原则始终需要记住:

  1. 单行访问是很慢的。特别是在机械硬盘中。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引用以提升效率。
  2. 按照顺序访问数据是很快的,主要有两个原因。第一,顺序I/O不需要多次磁盘寻道;第二,如果服务器能够按照需要顺序读取数据,那么就不再需要额外的排序操作。
  3. 索引覆盖是很快的。如果一个索引包含了查询所需的所有列,那么存储引擎就不需要再回表查找行。这避免了大量单行访问。

理解索引如果工作对优化查询非常重要。如果判断一个系统创建的索引是否合理?一般来说先按响应时间来对查询进行分析。找出消耗最长时间的查询或者那么给服务器带来最大压力的查询(查询性能分析方法),然后检查这些查询的schema、SQL和索引结构,判断是否有查询扫描了太多的行(通过explain),是否做了额外的排序或者使用了临时表,是否使用了随机I/O访问数据,或者是否有太多回表查询那些不在索引中的列的操作。

参考

[1] High Performance MySQL
[2] http://blog.codinglabs.org/articles/theory-of-mysql-index.html
[3] https://zh.wikipedia.org/wiki/B%E6%A0%91#%E6%90%9C%E7%B4%A2

上一篇 下一篇

猜你喜欢

热点阅读