MySQL 索引 | 青训营

46 阅读14分钟

1. 概述

索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

无索引查询会进行全表扫描

优缺点

2. 索引分类

  • 主键索引 PRIMARTY

设定为主键后数据库会自动建立索引,innodb为聚簇索引。默认自动创建,只能有一个

  • 常规索引

为了快速定位数据,有单列索引(一个索引包含一个列)和复合索引 / 联合索引(一个索引包含多个列)。可以有多个

  • 唯一索引 UNIQUE

索引列的值必须唯一,但允许有空值。可以有多个

  • 全文索引(My5.7版本之前只能由于MYISAM引擎)Full Text

全文索引类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR、TEXT类型上创建。查找的是文本中的关键字,不是比较索引中的值

根据索引的存储形式,又分为两种:

  • 聚集索引(clustered index):将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据。

必须有,而且只有一个

  • 二级索引(secondary Index):将数据与索引分开存储,索引结构的叶子节点是改列的值,关联的是对应的主键。

可以存在多个

聚集索引的选区规则:

  1. 如果存在主键,主键索引就是聚集索引。
  2. 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
  3. 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

图例:

聚集索引(一个表一个)就是用b+树存储主键,然后跟着行数据,所以我们根据主键查(我们一般用id)就比较快

二级索引就是非主键都是二级索引,每个节点存储指定的行的数据,下面跟着id

我们执行 select * from user where name = 'Arm';

流程如下,这里我们用二级索引,所以他先走name的索引,查到对应主键的值,再回到聚集索引里查行数据。

称之为回表查询。

第一行快,因为直接查聚集索引,不用回表查询

一页能存多少?指针占6B,主键占8B,那么我们设一行的主键数据数为n,那一行就是 8n + (n + 1)* 6 = 16 * 1024,就是n个主键,n+1个指针,算出需要1170个B,一页又有16个行,所以这一行 * 16,就是这一页的数据

3. 索引语法

3.1 基础语法

  • 创建索引
CREATE [UNIQUE|FULLTEXT] INDEX index_name ON table_name (index_col_name, ...);

如果只关联一个字段,我们称之单列索引,不然就叫组合索引联合索引

  • 查看索引
SHOW INDEX FROM table_name;
  • 删除索引
DROP INDEX index_name ON table_name;

3.2 性能分析

为了优化查询性能,通过性能分析,进行索引优化

3.2.1 sql执行频率

查看啊insert、update、delete、select的执行频次,查看sql执行的情况

SHOW [SESSION|GLOBAL] STATUS LIKE 'COM_______' -- 七个下划线是增删改查的内容

3.2.2 慢查询日志

慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。

MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

只记录慢查询记录,即超过long_query_time的才记录

配置完毕之后,通过以下指令重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息/var/lib/mysqL/localhost-slow.log。

查询慢查询开关情况

SHOW VARIABLE LIKE 'slow_query_log';

3.2.3 profile详情

show profiles能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。通过have_profiling参数,能够看到当前MySQL是否支持profile操作:

SELECT @@have_profiling;

默认profiling是关闭的,可以通过set语句在session/global级别开启profiling:

SET profiling = 1;

SHOW PROFILES; -- 查看当前session中,所有sql的执行情况
SHOW PROFILE FOR query_id; -- 查看指定query_id的sql语句各个阶段的耗时情况
SHOW PROFILE CPU FOR query query_id; -- 查看指定query_id的sql语句的cpu使用情况

3.2.4 explain执行计划

EXPLAIN或者DESC命令获取MySQL如何执行SELECT语句的信息,包括在SELECT语句执行过程中表如何连接和连接的顺序。

语法:

EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件; -- 直接在select语句之前加上关键字 explain/desc

EXPLAIN执行计划各字段的含义:

  • ld

select查询的序列号,表示查询中执行select子句或者是操作表的顺序(id相同,执行顺序从上到下;id不同,值越大,越先执行)。

  • select_type

表示SELECT的类型,常见的取值有SIMPLE((简单表,即不使用表连接或者子查询〉、PRIMARY(主查询,即外层的查询)、UNION (UNION中的第二个或者后面的查询语句)、SUBQUERY (SELECT/WHERE之后包含了子查询)等

  • type

表示连接类型,性能由好到差的连接类型为NULL(不涉及任何表)、system(访问系统表)、null MySQL不访问任何表或索引,直接返回结果、const(根据主键和唯一索引)、eq_ref、ref(使用了非唯一性的索引)、range(索引范围扫描,常用语<,<=,>=,between等操作)、index(索引全扫描)、all(全表扫描)。

  • possible_key

显示可能用到的索引,一个或多个

  • Key

实际使用的索引,如果为NULL,则没有使用索引。

  • Key_len

表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好。

  • rows

MySQL认为必须要执行查询的行数,在innodb引擎的表中,是一个估计值,可能并不总是准确的。

  • filtered

表示返回结果的行数占需读取行数的百分比, filtered的值越大越好。

连表查:id一致从上往下执行

子查询:先执行id值大的,即先执行子查询

前者根据唯一索引查,type为const,后者根据非唯一索引,type为ref

4. 索引使用

4.1 效率验证

不加索引

创建索引

4.1 ⭐最左前缀法则

如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。如果跳跃某一列,索引将部分失效(后面的字段索引失效)。

大头大哥不能死,中间兄弟不能断

有一个联合索引(创建的sql中顺序依次为profession,age, status)

1 生效,2生效,3生效,4失效,5失效

调换位置不影响:age,status,profession生效(优化器自动优化),但是中间和头也不能缺

范围查询:

联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效

前者索引生效但status失效,后者全部生效

4.3 ⭐最左前缀法则失效情况

  • 在索引列上进行函数操作、运算操作
-- 函数
explain select * from tb_user where substring(phone, 10, 2) = '15'; -- 失效,全表扫描
SELECT `sname` FROM `stu` WHERE concat(`sname`,'abc') ='Jaskeyabc';   -- 不会使用索引,因为使用了函数运算,原理与上面相同
SELECT `sname` FROM `stu` WHERE `sname`=concat('Jaskey','abc');      -- 会使用索引

-- 运算
SELECT `sname` FROM `t_stu` WHERE `age`=20;       -- 会使用索引
SELECT `sname` FROM `t_stu` WHERE `age`+10=30;    -- 不会使用索引!!因为所有索引列参与了计算
SELECT `sname` FROM `t_stu` WHERE `age`=30-10;    -- 会使用索引
  • 字符串不加引号
explain select * from tb_user where phone = 12345678999; -- 存在隐式类型转换,phone为varchar类型

explain select * from tb_user where phone = '12345678999'; -- 走索引

所以当需要搜索email列中.com结尾的字符串而email上希望走索引时候,可以考虑数据库存储一个反向的内容reverse_email

SELECT * FROM `table` WHERE `reverse_email` LIKE REVERSE('%.com'); -- 走索引

注:以上如果你使用REVERSE(email) = REVERSE('%.com'),一样得不到你想要的结果,因为你在索引列email列上使用了函数,MySQL不会使用该列索引 同样的,索引列上使用正则表达式也不会走索引。

  • 模糊查询

如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

explain select * from tb_user where profession like '软件%'; -- √
explain select * from tb_user where profession like '%工程'; -- ×
explain select * from tb_user where profession like '%工程'; -- ×
  • or连接的条件

用or分割开的条件,如果or前的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到(涉及的所有索引必须都建立索引)

explain select * from tb_user where id = 10 or age = 23; -- or后面部分没有索引,都不走索引
explain select * from tb_user where phone = '17799990017' or age = 23; -- 有联合索引,但是age没有自己的索引,都不走索引

所以除非每个列都建立了索引,否则不建议使用OR,在多列OR中,可以考虑用UNION 替换

select * from dept where dname='jaskey' union
select * from dept where loc='bj' union
select * from dept where deptno=45

为什么:因为当符合最左前缀法则时,是直接走索引,但是遇上失效情况会变成:索引扫描+全表扫码+合并

  • order by

在ORDER BY操作中,排序的列同时也在WHERE中时,MYSQL将无法使用索引;

  • 数据分布影响

如果MySQL评估使用索引比全表更慢,则不使用索引。

4.4 SQL提示

SQL提示,是优化数据库的一个重要手段,简单来说,就是在5QL语句中加入一些人为的提示来达到优化操作的目的。

两个索引候选,优化器自动选择其一

  • use index:
explain select *from tb_user use index(idx_user_pro) where profession = '软件工程';
  • ignore index:
explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';
  • force index:
explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';

4.5 覆盖索引

尽量使用覆盖索引(查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到),减少select * 。

explain的extra字段:

  • using index condition:查找使用了索引,但是需要回表查询数据
  • using where; using index:查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据

  1. 直接走聚集索引,索引覆盖
  2. 走二级索引,索引覆盖
  3. 走二级索引,但是select的内容不只有二级索引所对应的列,那找到id后再回表走聚集索引,回表查询

创建索引(username,password),可以把这两个和id一起查出

4.6 前缀索引

当字段类型为字符串(varchar,text等)时,有时候需要索引很长的字符串,这会让索引变得很大,查询时,浪费大量的磁盘lO,影响查询效率。此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。

  • 语法:
create index idx_xxxx on table_name(column(n));
  • 长度:

可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

select count(distinct email) / count(*) from tb_user;
select count(distinct substring(email,1,5)) / count(*) from tb_user;

在前缀索引中查时,查可能会查到多个,所以还会回表到row中去比对是否一致

4.7 单列索引与联合索引

单列索引:即一个索引只包含单个列。

联合索引:即一个索引包含了多个列。

在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。

覆盖索引,不回表

5. 索引设计

  1. 针对于数据量较大,且查询比较频繁的表建立索引。
  2. 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
  3. 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
  4. 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
  5. ⭐尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
  6. ⭐要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
  7. 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。

6. 索引结构

MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的结构,主要包含以下几种:

我们平常所说的索引,如果没有特别指明,都是指B+树结构组织的索引。

二叉树、红黑树

B-tree

最大度数为n,意味着有n个子节点的节点就有n-1个节点,n个指针,因为n个指针能最多指向n个子节点

新增如果导致节点度数大于最大度数,中间值向上分裂

⭐B+tree

所有的数据都出现在叶子节点,有双向链表

7. 索引下推

在查询过程中尝试将部分计算或过滤操作下推到存储引擎的数据层(原先是在server层中),以减少从数据库中检索和传输的数据量,从而提高查询性能。

案例:

使用一张用户表tuser,表里创建联合索引(name, age)。

如果现在有一个需求:检索出表中名字第一个字是张,而且年龄是10岁的所有用户。那么,SQL语句是这么写的:

select * from tuser where name like '张%' and age=10;

根据索引最左匹配原则,那么就知道这个语句在搜索索引树的时候,只能用 张,找到的第一个满足条件的记录id为1。

那接下来的步骤是什么呢?

没有使用ICP

在MySQL 5.6之前,存储引擎根据通过联合索引找到name like '张%' 的主键id(1、4),逐一进行回表扫描,去聚簇索引找到完整的行记录,server层再对数据根据age=10进行筛选。

我们看一下示意图:

可以看到需要回表两次,把我们联合索引的另一个字段age浪费了。

使用ICP

而MySQL 5.6 以后, 存储引擎根据(name,age)联合索引,找到name like '张%',由于联合索引中包含age列,所以存储引擎直接再联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描。

我们看一下示意图:

可以看到只回表了一次。

除此之外我们还可以看一下执行计划,看到Extra一列里 Using index condition,这就是用到了索引下推。

索引下推使用条件

  • 只能用于range、 ref、 eq_ref、ref_or_null访问方法;
  • 只能用于InnoDB和 MyISAM存储引擎及其分区表;
  • 对InnoDB存储引擎来说,索引下推只适用于二级索引(也叫辅助索引);

索引下推的目的是为了减少回表次数,也就是要减少IO操作。对于InnoDB的聚簇索引来说,数据和索引是在一起的,不存在回表这一说。

  • 引用了子查询的条件不能下推;
  • 引用了存储函数的条件不能下推,因为存储引擎无法调用存储函数。

8. 小结