索引

什么是索引

索引是一种方便我们高效查找某一列或几列数据的一种数据结构,一般是 B+树或者 hash树。想象一下在一个表中有一列是我们经常需要用于作为查询条件的列,也就是它经常出现在 where 子句中,那么如果每次用到它都要顺序遍历全表数据来找到我们所需要的那一行,听着好像效率不太高的样子,所以就出现了索引这个东西。
因为索引一般是使用树这种数据结构来存储的,而树是对排序很友好的一种数据结构,例如一个二叉树,左边都是比根小的而右边都是比根大的,要查找一个数据就很容易。所以有了索引之后就可以增加检索的效率,大大缩短查找时间。

索引的创建与删除

创建索引

可以在创建表的时候一起创建索引,也可以在建完表之后单独创建
在建表的时候创建索引:

CREATE TABLE `tb` (
  `tid` int(3) NOT NULL,
  `tname` varchar(10) DEFAULT NULL,
  `test_column` int(3) DEFAULT NULL,
  PRIMARY KEY (`tid`),
  KEY `name_index` (`tname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

以上语句创建了一个名为 tb 的表(表创建完成之后可以通过以下SQL语句来查看创建该表所需要的SQL语句:

show create table 表名;

我们创建了 tb 表,并指定了 主键为 tid 字段,在 tname 列创建了一个名为 name_index 的索引,并指定了引擎为 InnoDB、字符编码方式为 utf8mb4。

在建表后通过 alter 语句或 create 语句来创建索引:

alter table 表名 add index 索引名(列1, 列2, 列3...);
create index 索引名 on 表名(列1, 列2, 列3...);

可以对一个或多个列共同添加索引。索引创建完成后可以通过以下语句来查看该表的所有索引信息:

MariaDB [sql_optimize]> show index from tb;
+-------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name   | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| tb    |          0 | PRIMARY    |            1 | tid         | A         |           3 |     NULL | NULL   |      | BTREE      |         |               |
| tb    |          1 | name_index |            1 | tname       | A         |           3 |     NULL | NULL   | YES  | BTREE      |         |               |
+-------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)

可以看到主键也是一个索引

删除索引

有两种删除索引的方式:

drop index 索引名 on 表名;
alter table 表名 drop index 索引名;

索引的使用

使用explain分析SQL语句

使用索引的时候有几点需要注意的地方来避免让索引失效,要观察索引是否失效可以通过 explain 语句来查看 SQL 语句的执行情况。

MariaDB [sql_optimize]> explain select * from tb t1 where exists (select t2.tid from tb2 t2 where t1.tid = t2.tid);
+------+--------------+-------------+--------+---------------+---------+---------+---------------------+------+-------------+
| id   | select_type  | table       | type   | possible_keys | key     | key_len | ref                 | rows | Extra       |
+------+--------------+-------------+--------+---------------+---------+---------+---------------------+------+-------------+
|    1 | PRIMARY      | <subquery2> | ALL    | distinct_key  | NULL    | NULL    | NULL                |    1 |             |
|    1 | PRIMARY      | t1          | eq_ref | PRIMARY       | PRIMARY | 4       | sql_optimize.t2.tid |    1 |             |
|    2 | MATERIALIZED | t2          | ALL    | NULL          | NULL    | NULL    | NULL                |    1 | Using where |
+------+--------------+-------------+--------+---------------+---------+---------+---------------------+------+-------------+
3 rows in set (0.00 sec)

id
数值越大执行顺序越靠前,数值一样时从上往下顺序执行,在本例中也就是 t2 -> subquery2 -> t1。

select_type
查询类型,取值有SIMPLE(简单查询,不包含子查询或 union)、PRIMARY(主查询,一般出现在有子查询的语句中)等。

table
使用的表,有时候会有一些临时表,比如这里的 subquery2。

type
类型,这个类型和上面的 select_type 不要一样,这个 type 字段可以看成是 SQL 语句执行速度的一个衡量方式,
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说 system 和 const 是达不到的,eq_ref 也是比较困难的,所以我们一般能达到的是 ref、range 和 index,当然这些都是针对有索引的情况来说的,没有索引的话那就只能是 ALL。

possible_keys 和 key
预测会使用的索引和实际使用的索引

extra
一些额外信息,比较常见的几种有

  • using filesort:需要额外一次排序,常见于有 order by的语句中
  • using temporary:用到了临时表,常见于有 group by 的语句中
  • using index:代表使用了索引
  • using where:意味不明

前两种代表性能消耗较大,是我们需要避免的,如果出现了这两个信息说明我们的 SQL 语句需要优化了,using index 意味着性能有所提升,而 using where 的出现好像很难总结出什么规律,一般不太需要关注它。

最佳左前缀

这个是针对复合索引来说的,也就是一个索引中包含多个列的时候。最佳左前缀的意思是我们使用索引的时候要按照复合索引的顺序来使用,不要跨列,也就是说,如果一个索引的定义是(a,b,c,d),那我们使用的时候就要按照 abc 的顺序来使用。说到这个使用顺序就要提到 SQL 的解析过程了
编写过程:

select dinstinct  ..from  ..join ..on ..where ..group by ...having ..order by ..limit ..

解析过程:

from .. on.. join ..where ..group by ....having ...select dinstinct ..order by limit ...

按照这个解析过程,这样的一条 SQL 语句是符合最佳左前缀的:

select d from tb where a=... and b=... and c=...;

我们同时使用了 abc 这三个字段,并且解析顺序也会是 a -> b -> c -> d
这样的 SQL 语句是不符合最佳左前缀的,它会使得一部分索引失效:

select d from tb where a=... and c=...;

b 列没有使用到,也就是说我们只用了 acd 这三列,跨了 b 列,这条语句会导致 a 后面的索引都失效,也就是只有 a 使用到了索引, c=... 语句并没有使用索引。
举个例子:

MariaDB [sql_optimize]> explain select b4 from test03 where b1=1 and b2=1 and b3=1;
+------+-------------+--------+------+-------------------+-------------------+---------+-------------------+------+-------------+
| id   | select_type | table  | type | possible_keys     | key               | key_len | ref               | rows | Extra       |
+------+-------------+--------+------+-------------------+-------------------+---------+-------------------+------+-------------+
|    1 | SIMPLE      | test03 | ref  | index_b1_b2_b3_b4 | index_b1_b2_b3_b4 | 14      | const,const,const |    1 | Using index |
+------+-------------+--------+------+-------------------+-------------------+---------+-------------------+------+-------------+
1 row in set (0.00 sec)
MariaDB [sql_optimize]> explain select b4 from test03 where b1=1 and b3=1;
+------+-------------+--------+------+-------------------+-------------------+---------+-------+------+--------------------------+
| id   | select_type | table  | type | possible_keys     | key               | key_len | ref   | rows | Extra                    |
+------+-------------+--------+------+-------------------+-------------------+---------+-------+------+--------------------------+
|    1 | SIMPLE      | test03 | ref  | index_b1_b2_b3_b4 | index_b1_b2_b3_b4 | 4       | const |    1 | Using where; Using index |
+------+-------------+--------+------+-------------------+-------------------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

可以看到第二个 SQL 语句中跨了 b2 列,所以 index_b1_b2_b3_b4 部分失效了(索引是否部分失效可以通过 key_len 字段看出来)。

索引覆盖

覆盖索引(covering index ,或称为索引覆盖)即从非主键索引中就能查到的记录,而不需要查询主键索引中的记录,避免了回表的产生减少了树的搜索次数,显著提升性能。
尽量不要使用 SELECT *语句,因为这样会发生回表查询不能使用索引覆盖从而导致查询效率低。观察以下两条 SQL 语句,一个是 SELECT * 一个是只选择需要的列:

MariaDB [sql_optimize]> explain select * from test02 where a4=4 and a6=4;
+------+-------------+--------+------+----------------------+----------+---------+-------+------+-------------+
| id   | select_type | table  | type | possible_keys        | key      | key_len | ref   | rows | Extra       |
+------+-------------+--------+------+----------------------+----------+---------+-------+------+-------------+
|    1 | SIMPLE      | test02 | ref  | index_a4,index_a4_a6 | index_a4 | 5       | const |    1 | Using where |
+------+-------------+--------+------+----------------------+----------+---------+-------+------+-------------+
1 row in set (0.00 sec)
MariaDB [sql_optimize]> explain select a4,a6 from test02 where a4=4 and a6=4;
+------+-------------+--------+------+----------------------+-------------+---------+-------------+------+-------------+
| id   | select_type | table  | type | possible_keys        | key         | key_len | ref         | rows | Extra       |
+------+-------------+--------+------+----------------------+-------------+---------+-------------+------+-------------+
|    1 | SIMPLE      | test02 | ref  | index_a4,index_a4_a6 | index_a4_a6 | 10      | const,const |    1 | Using index |
+------+-------------+--------+------+----------------------+-------------+---------+-------------+------+-------------+
1 row in set (0.00 sec)

可以看到使用SELECT *的语句执行时没有走复合索引(即 index_a4_a6,这是由 a4 和 a6 功能组成的一个复合索引),而是走了 index_a4 这个只有 a4 组成的索引,而使用 SELECT a4, a6的语句则走了复合索引,因为整条SQL 语句就只用到了 a4 和 a6 这两列,这两列在index_a4_a6 存储了,所以不需要回表查询,查一次这个复合索引就可以拿到结果了,而前面的SELECT *语句还需要回表查询那些索引里没有字段。所以说尽量不要使用SELECT *,需要用到什么字段就 select 什么字段,避免索引覆盖失效同时也可以减少 IO 消耗。

避免对索引列进行额外运算

对索引进行额外的运算(加减乘、类型转换等)会导致索引失效:

MariaDB [sql_optimize]> explain select a4 from test02 where a4*2=4;
+------+-------------+--------+-------+---------------+----------+---------+------+------+--------------------------+
| id   | select_type | table  | type  | possible_keys | key      | key_len | ref  | rows | Extra                    |
+------+-------------+--------+-------+---------------+----------+---------+------+------+--------------------------+
|    1 | SIMPLE      | test02 | index | NULL          | index_a4 | 5       | NULL |    4 | Using where; Using index |
+------+-------------+--------+-------+---------------+----------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
MariaDB [sql_optimize]> explain select a4 from test02 where a4=4;
+------+-------------+--------+------+----------------------+----------+---------+-------+------+-------------+
| id   | select_type | table  | type | possible_keys        | key      | key_len | ref   | rows | Extra       |
+------+-------------+--------+------+----------------------+----------+---------+-------+------+-------------+
|    1 | SIMPLE      | test02 | ref  | index_a4,index_a4_a6 | index_a4 | 5       | const |    1 | Using index |
+------+-------------+--------+------+----------------------+----------+---------+-------+------+-------------+
1 row in set (0.00 sec)

可以看到 type 从ref 退化成了 index,并且 row 是 4 说明发生了回表查询(test02 表中一共4条数据)。

SQL语句优化

前面我们已经说了索引的重要性了,所以 SQL 优化的很大一部分就是索引的优化,当然还有一些其他的优化原则,这就是我们本节要讲的东西。

小表驱动大表

这个原则不只是写 SQL 语句需要遵循,我们平时写代码的时候也要尽量遵循这个原则。比如写双层 for 循环的时候,尽量把循环次数小的那个 for 放在外层而循环次数多的放在内层,这样就可以减少从内外侧循环切换的次数,减少一些性能消耗。
举个例子,两个 for 循环,一个要循环10次一个要循环100次,当然不管两个循环怎么组合最终都是一共要循环1000次,但是如果把循环10次的放在外层,那么就从外层循环跳到内层循环的次数就只要10次,反之要100次。所以把循环次数少的那个 for 循环放在外面可以减少栈帧的切换次数从而提升性能。
回到 SQL 场景中就是当存在子查询的时候,把数据量大的表放在子查询里而数据量小的表放在主查询里。当然可能有的场景下我们就是必须得把大表放在主查询里,因为我们需要的字段在大表里,那么这时候我们就可以使用 existsin 这两个关键词来做一些转换来提升 SQL 语句的效率了:
首先说一下 inexists的区别:

  • in: 先查子查询,查出结果后和主查询做笛卡尔积,子查询只查一次。
  • exists: 先查主查询,然后每次进行主查询的时候都会遍历一遍子查询表,也就是说子查询执行次数为主查询表中的数据量n。

假设现在t1为小表,t2为大表
小表在外层时
正例:

select * from t1 where exists(select id from t2 where id=t1.id);

反例:

select * from t1 where id in (select id from t2);

正例之所以是推荐的写法是因为 exists后面跟了大表,此时子查询可以使用 t2 表的 id 列的索引来提高查询效率,外层是一个小表,内层又可以使用索引来加速查询,所以总体效率较高;而反例之所以不推荐是因为 in 后面跟了大表,所以这个大表会被整个查询一遍和t1做笛卡尔积,很显然没有正例的效率高。
大表在外层时
正例:

select * from t2 where id in (select id from t1);

反例:

select * from t2 where exists(select id from t1 where id=t2.id);

反例exists后跟小表,索引对于小表的查询效率提升不明显。
总结起来就是 in后面跟小表,exists后面跟大表