索引概述

mysql的索引数据结构主要是采用B+tree、Hash 2种。

B+tree 数据存储在叶子节点上,非叶子节点主要是起到了索引的作用,叶子节点与叶子节点之间采用双向链表的方式方便进行范围查询以及排序功能。

Hash 哈希索引使用的是哈希算法,将键值设置在hashcode对应的槽位上,如果出现哈希碰撞,采用链表进行存储。与java中HashMap数据结构类似,但是哈希索引在排序或者区间查找等场景可能会让SQL变得更慢。

创建索引语句格式如下

create index 索引名称 on 表名(字段名); -- 创建一个单列索引
create index 索引名称 on 表名(字段名,字段名,字段名);-- 创建联合索引/多列索引
create index 索引名称 on 表名(字段名 排序规则asc | desc); -- 创建一个索引 并指定排序规则、
create index 索引名称 on 表名(列名(长度)) ;-- 创建前缀索引

索引的分类

从索引的存储形式上分为:

  • 聚集索引
    索引和整行数据(Row)存储在一起,每一行数据有且只有一个。默认情况主键索引就是聚集索引,没有主键索引的情况下,第一个唯一索引就是聚集索引,如果唯一索引也没有,mysql默认会生成一个隐藏的rowId作为聚集索引。查询走到聚集是不需要回表操作,所以性能很高。
  • 二级索引
    非聚集索引为二级索引,将数据和索引分开存储,二级索引只对应了数据行的主键,一张表可以多个这样的索引,需要回表操作。

按类型区分主要分为:

  • 主键索引 primary 只能有1个

  • 唯一索引 unique 可以有多个

  • 普通索引 常规的hash、 b+tree索引

  • 全文索引 Mysql用得比较少 主要是用来全文搜索

  • 联合索引 多个字段组合成一个索引 比较常用

如何在mysql中查看各个语句操作的执行频率次数呢?

通过 show GLOBAL status like ‘Com_______’语句能查看各个操作的次数,此指令能看到数据库中主要以查询为主还是更新为主

mysql> show GLOBAL status like 'Com_______';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_binlog | 0 |
| Com_commit | 6 |
| Com_delete | 14 |
| Com_insert | 509 |
| Com_repair | 0 |
| Com_revoke | 0 |
| Com_select | 2319 |
| Com_signal | 0 |
| Com_update | 38 |
| Com_xa_end | 0 |
+---------------+-------+
10 rows in set (0.00 sec)

启用慢SQL

slow_query_log= on 开启慢查询日志
slow_query_log_file = /opt/mysqlslow.log #慢查询日志文件
long_query_time= 4 #指定多少时间后为慢查询 4秒为慢查询 默认为10秒

针对慢查询优化是处理SQL优化的重要手段,查看慢sql日志文件 /opt/mysqlslow.log就能看到系统中慢sql的执行情况,找到了慢SQL就可以着手进行优化。

启用Mysql Profiling

Profiling是5.0之后的版本才被支持的特性。但是又在mysql5.7之后,mysql推荐使用performance schema

使用@@have_profiling变量查看当前数据库是否支持profiling。如下返回YES证明支持。

mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES |
+------------------+
1 row in set, 1 warning (0.00 sec)

使用流程大致如下

set profiling=1; #打开分析统计开关
# select * from user;执行你的任何SQL语句
show profiles;
mysql> show profiles; # 能看到上面语句的执行时间
+----------+------------+--------------------+
| Query_ID | Duration | Query |
+----------+------------+--------------------+
| 1 | 0.00024075 | select * from user |
+----------+------------+--------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show profile for query 1; # 查看queryid为1的更详细的性能分析
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000064 |
| checking permissions | 0.000007 |
| Opening tables | 0.000018 |
| init | 0.000023 |
| System lock | 0.000007 |
| optimizing | 0.000004 |
| statistics | 0.000012 |
| preparing | 0.000010 |
| executing | 0.000003 |
| Sending data | 0.000049 |
| end | 0.000006 |
| query end | 0.000007 |
| closing tables | 0.000007 |
| freeing items | 0.000015 |
| cleaning up | 0.000010 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)

explain 语句执行性能分析

在mysql中explain应该是使用频次最多,最为重要的性能分析工具。

语法 explain ‘sql语句’。如:

mysql> explain select * from user;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | user | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
字段含义
idid表示执行顺序 值越大越先执行,值相同从上往下的顺序执行
select_type查询类型
simple:表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple,且只有一个
primary:一个需要union操作或者含有子查询的select,位于最外层的单位查询的select_type即为primary。 且只有一个
union:union连接的两个select查询,第一个查询是dervied派生表,除了第一个表外,第二个以后的表 select_type都是union
dependent union:与union一样,出现在union 或union all语句中,但是这个查询要受到外部查询的影响
union result:包含union的结果集,在union和union all语句中,因为它不需要参与查询,所以id字段为null
subquery:除了from字句中包含的子查询外,其他地方出现的子查询都可能是subquery
dependent subquery:与dependent union类似,表示这个subquery的查询要受到外部表查询的影响
derived:from字句中出现的子查询,也叫做派生表,其他数据库中可能叫做内联视图或嵌select
table表名 有别名的时候显示的是别名
partitions分区 主要是针对分区表才会有显示此字段
type访问类型 性能优化的重要指标,从高到低顺序为system -> const -> eq_ref -> ref -> range -> index ->ALL
const: 当使用主键或者唯一索引查询时会出现const
ref 使用非唯一性索引会出现ref
range 当使用范围查询索引的时候会出现range
all 全表扫描 需要优化
possible_keys可能会用到的索引
key实际用到的索引,如果是联合索引这里是显示用到了多个索引的列表 逗号分隔
key_lenkey的长度
ref如果是使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段,如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func
rows根据表预估统计行数及索引选用情况,估算的找到所需的记录所需要读取的行数,值越小越好
filtered被过滤的比例 值越大性能越好
Extramysql执行语句额外信息
Using index:该值表示相应的select操作中使用了覆盖索引(Covering Index)
Using where:表示MySQL服务器在存储引擎收到(使用索引)记录后进行“后过滤”
Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
Using filesort: MySQL中无法利用索引完成的排序操作称为“文件排序”,常见于order by和group by语句中

创建2张测试表 user、score来简单的测试一下索引规则

CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`sex` int(2) DEFAULT NULL,
`age` int(10) DEFAULT NULL,
`create_time` datetime DEFAULT now(),
PRIMARY KEY (`id`),
KEY `name_idx` (`name`),
KEY `age_idx` (`age`),
KEY `create_time_idx` (`create_time`)
) ENGINE=InnoDB;

CREATE TABLE `score` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course` varchar(68) CHARACTER SET utf8 DEFAULT NULL,
`score` double(10,2) DEFAULT NULL,
`user_id` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id_idx` (`user_id`)
) ENGINE=InnoDB;

-- 插入user表
insert user (name,sex,age) values('a1',1,10),('a2',0,11),('a3',0,13),('a4',1,15),('a5',1,16),('a6',0,17),('a7',1,18),('a8',0,19),('a9',0,20),('a10',1,21),('a11',0,22);
-- 插入score表
insert score (course,score,user_id) values('语文',99,1),('数学',60,1),('英语',89,1),('物理',78,1);
explain结果中id执行顺序

根据用户name=a1查询成绩表

explain select * from score where user_id = (select id from user where name=’a1’);

+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| 1 | PRIMARY | score | NULL | ALL | user_id_idx | NULL | NULL | NULL | 4 | 100.00 | Using where |
| 2 | SUBQUERY | user | NULL | ref | name_idx | name_idx | 153 | const | 1 | 100.00 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+

可以看到user表的id为2, score表的id为1。说明user要需要先执行 然后再执行user表。事实也是如此,有子查询的情况,理论上也是需要先执行子查询。

更多explain参考 explain详解

联合索引

联合索引也叫多列索引 也是比较常用且非常实用的索引。联合索引遵循最左匹配原则,也就是联合索引中最左边的字段必须存在,一级一级的查找,中间不能跨列,依次向右匹配。下面为user表中创建一个由字段name,age,sex组成的联合索引。

ALTER TABLE user drop index name_idx; -- 删除索引 
ALTER TABLE user drop index age_idx; -- 删除索引
create index name_age_sex on user (name,age,sex);
show index from user;-- 查看索引列表

explain select * from user where sex=1 and name=’a1’ and age=11 ;

explain select * from user where name=’a1’ and age=11 and sex=1;

以上2语句都能成功使用到完整的索引,说明索引在查找的时候和语句的条件顺序没有太大关系,主要是要遵循创建联合索引的最左原则就行。

+-------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------------+
| table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+-------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------------+
| user | NULL | ref | name_age_sex | name_age_sex | 163 | const,const,const | 1 | 100.00 | Using index |
+-------+------------+------+---------------+--------------+---------+-------------------+------+----------+-------------+

explain select * from user where age=11 ;

该语句中由于没有遵循最左匹配原则,直接加了一个age,实际没有使用到联合索引

+-------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+-------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| user | index | NULL | NULL | NULL | NULL | 11 | 10.00 | Using where; |
+-------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

explain select * from user where name=’a1’ and age>10 and sex=1;

该语句中使用了联合索引的前2个字段索引,最后一个sex并没有使用到,是因为age>10语句执行之后 后面的条件就不继续查找索引。同理还有小于<也是一样。

+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| user | NULL | range | name_age_sex | name_age_sex | 158 | NULL | 1 | 10.00 | Using index condition |
+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

explain select * from user where name=’a1’ and age>=10 and sex=1;

和上一步语句中>符变为了>= 能看到结果中key_len变长了 走到3个字段的索引。足以证明>后面的字段不走索引,那么>=却走到了索引。

+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+
| user | NULL | range | name_age_sex | name_age_sex | 163 | NULL | 1 | 10.00 | Using index condition |
+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------+

总结一下索引失效的部分案例,失效的部分场景实在是太多了。

  1. 联合索引的时候 没有按照最左匹配
    如上面的示例中 explain select * from user where age=11 ;
  2. 字段值类型传递错误 如varchar 不加’ ‘号 导致出现隐式类型转换,在高版本的mysql又是生效的
  3. 字段上写函数 如date_format等 不走索引
  4. >号后面的条件 不走索引,>=可以走 同理 <号后面的条件不走索引,<=走索引
  5. or 后字段如果没有索引会导致有索引的where,不走索引
  6. 数据分布 评估,如果走索引的时候数据反而更多,mysql就不会走索引
  7. is not null ,is null 也会走数据分布评估逻辑,如果数据量null不多,那么is not null 不走。

覆盖索引

覆盖索引是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 ,就不需要回表操作,如果查询的列在索引中找不到那么mysql就会进行回表操作。所以查询的时候应该尽可能的使用覆盖索引,减少select * 这样的查询方式。

当用explain 分析的时候 Extra出现如下 using index condition表示进行了回表。

using index condition -- 使用了索引 但是进行了回表查询

没有回表操作的样例

using where
using index -- 查找使用索引。select的字段都有索引 不需要回标查询

前缀索引

前缀索引主要是针对文本字段如longtext、text、varchar 之类的类型创建索引,如果只是对大字段创建索引可能造成索引非常大,查询与存储都浪费IO,影响查询性能,前缀索引正好可以解决这个问题,可以对文本前多少个字符创建索引,这样就避免了全部文本创建索引。这样既优化了存储大小,也优化了查询IO的问题,使用此索引方式需要具体看业务场景是否可以允许只搜索前多少字符这样的需求。

创建前缀索引格式如下:

create index 索引名称 on 表名(字段名(长度));

SQL语句预定义SQL索引指示

主要分为预置SQL语句,指定索引、忽略索引、强制索引。但是这里只是向mysql推荐,至于最终是不是使用这个索引 需要数据库引擎最终判断。

  • use index 建议语句指定sql使用指定索引

    explain select * from score use index(user_idx) where user_id=1;

  • ignore index 忽略语句不使用指定索引
    explain select * from score ignore index(user_id_idx) where user_id=3;

  • force index 强制语句使用指定索引
    explain select * from score force index(user_id_idx) where user_id=3;

索引设计原则

  • 数据量大 且查询频繁的建议索引

  • where, order by ,groupby建立索引

  • 字符串太长 可以考虑前缀索引

  • 选择区分度高的字段创建索引

  • 区分度不高的字段可以不建索引 如性别 永远只有男、女

  • 多字段查询尽量使用联合索引、select字段尽量使用覆盖索引 避免回表

  • 如果索引列不为NULL 建议字段设置NOT NULL。对mysql索引优化器有好处

  • 索引不要建的太多,满足查询需求就好,太多会影响insert update delete这些更新语句的性能,多硬盘也不友好,会占用大量存储