索引简介

定义

MySQL官方对索引的定义为:索引是帮助MySQL高效获取数据的数据结构。

可以得到索引的本质:索引是数据结构。

简单理解:排好序的快速查找数据结构。

一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。

我们平常所说的索引,如果没有特别指明,都是指B树(多路搜索树,不一定是二叉树)结构的索引。

其中聚集索引,次要索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。除了B+树这种类型的索引之外,还有哈希索引等。

优势

类似于大学图书馆建数目索引,提高数据检索的效率,降低数据库的IO成本。

通过索引列队数据进行排序,降低数据排序的成本,降低了CPU的消耗。

劣势

虽然索引大大提高了查询速度,同时会降低表的更新速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息。

索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询。

索引分类

单值索引

一个索引列只包含单个列,一个表可以有多个索引。

唯一索引

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

复合索引

一个索引包含多个列。

覆盖索引

SQL只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据,即查询字段为索引字段。

基本语法

  • 创建
  1. CREATE [UNIQUE] INDEX <index_name> ON <table_name>(<column_name>(length));
  2. ALTER TABLE <table_name> ADD [UNIQUE] INDEX <index_name> ON (<column_name>(length));
  • 删除
  1. DROP INDEX <index_name> ON <table_name>;
  • 查看
  1. SHOW INDEX FROM <table_name>;
  • ALTER
  1. -- 添加主键,即唯一索引
  2. ALTER TABLE <table_name> PRIMARY KEY (column_list);
  3. -- 创建唯一索引
  4. ALTER TABLE <table_name> UNIQUE <index_name>(column_list);
  5. -- 添加普通索引
  6. ALTER TABLE <table_name> INDEX <index_name>(column_list);
  7. -- 指定全文索引
  8. ALTER TABLE <table_name> FULLTEXT index_name>(column_list);

索引结构

索引

  • BTREE
  • HASH
  • FULL-TEXT
  • R-TREE

需要建立索引的情况

  • 主键自动建立唯一索引
  • 频繁作为查询条件的字段应该创建索引
  • 查询中与其它表关联的字段,外键关系建立索引
  • 频繁更新的字段不适合创建索引
  • Where条件里用不到的字段不创建索引
  • 单值/组合索引的选择,在高并发下倾向创建组合索引
  • 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
  • 查询中统计或者分组字段

不需要建立索引的情况

  • 表记录太少 [原因:低于百万数的表MySQL还是扛得住的。]
  • 经常增删改的表 [原因:提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。]
  • 数据重复且分布均匀的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引。注意,如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果 [例如:性别等字段。]

Join查询

join说明

  • LEFT JOIN:返回左表中的所有记录和右表中联结字段相等的记录。
    • 格式:SELECT ... table1 LEFT JOIN table2 ON ...
    • 说明:会取得table1全部记录,即使table2没有匹配记录
  • RIGHT JOIN:返回右表中的所有记录和左表中联结字段相等的记录。
    • 格式:SELECT ... table1 RIGHT JOIN table2 ON ...
    • 说明:会取得table2全部记录,即使table1没有匹配记录
  • INNER JOIN:只返回两个表中联结字段相等的记录。
    • 格式:SELECT ... table1 INNER JOIN table2 ON ...
    • 说明:会取得table1table2联结字段相等的记录

join图示

  1. SELECT <select_list> from table_a a LEFT JOIN table_b b ON a.key = b.key

image-20210314221223407.png

  1. SELECT <select_list> from table_a a LEFT JOIN table_b ON a.key == b.key WHERE b.key is NULL

image-20210314222433261.png

  1. SELECT <select_list> from table_a a INNER JOIN table_b b ON a.key = b.key

image-20210314221536479.png

  1. SELECT <select_list> from table_a a RIGHT JOIN table_b b ON a.key = b.key

image-20210314221440655.png

  1. SELECT <select_list> from table_a a RIGHT JOIN table_b ON a.key = b.key WHERE a.key is NULL

image-20210314222235183.png

  1. -- Oracle支持 FULL OUTER JOIN,但是MySQL不支持
  2. -- Oracle
  3. SELECT <select_list> FROM table_a a FULL OUTER JOIN table_b b ON a.key = b.key
  4. -- MySQL
  5. SELECT <select_list> from table_a a LEFT JOIN table_b b ON a.key = b.key
  6. union
  7. SELECT <select_list> from table_a a RIGHT JOIN table_b b ON a.key = b.key

image-20210314222826674.png

  1. # Oracle
  2. SELECT <select_list> FROM table_a a FULL OUTER JOIN table_b b ON a.key = b.key WHERE a.key is NULL or b.key is NULL
  3. # MySQL
  4. SELECT <select_list> from table_a a LEFT JOIN table_b b ON a.key = b.key where b.id is null
  5. union
  6. SELECT <select_list> from table_a a RIGHT JOIN table_b b ON a.key = b.key where a.id is null

image-20210314222858303.png

SQL执行顺序

  • 手写
  1. SELECT DISTINCT
  2. <select_list>
  3. FROM
  4. <left_table> <join_table>
  5. JOIN <right_table> ON <join_condetion>
  6. WHERE
  7. <where_condition>
  8. GROUP BY
  9. <group_by_list>
  10. HAVING
  11. <having_condition>
  12. ORDER BY
  13. <order_by_condition>
  14. LIMIT <limit_number>
  • 机读

image.png

性能分析

MySQL Query Optimizer

MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供他认为最优的执行计划(它认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)
当客户端向MySQL请求一条Query,命令解析器模块完成请求分类,区别出是SELECT并转发给MySQL Query Optimizer时,MySQL Query Optimizer 首先会对整条Query进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对Query 中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析Query 中的Hint 信息(如果有),看显示Hint信息是否可以完全确定该Query的执行计划。如果没有Hint或Hint信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划。

MySQL性能瓶颈

CPU: CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据的时候

IO: 磁盘I/O瓶颈发生在装入数据远大于内存容量的时候

服务器硬件的性能瓶颈:topfreeiostatvmstat来查看系统的性能状态

Explain

官网介绍:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html

使用目的:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 可以使用的索引
  • 实际使用的索引
  • 表之间的引用
  • 每张表被优化器查询的行数

使用方法:explain + sql语句

字段解释

字段 描述
id 查询序列号
select_type 查询的类型
partitions 匹配的分区
type 表的连接类型
possible_keys 可能使用的索引
key 实际使用的索引
key_len 索引字段的长度
ref 列与索引的比较
rows 预估读取的行数
filtered 按表条件过滤的行百分比
Extra 执行情况的描述和说明

(1)id

解释:
select查询的序列号,包含一组数字,表示查询中执行的select子句或操作表的顺序。

说明:

  • id相同,执行顺序由上至下;
  • id不同,如果是子查询,id递增,id值越大优先级越高,越先被执行;
  • id相同不同,同时存在,id如果相同,可以认为是一组,从上往下顺序执行,在所有组中,id值越大,优先级越高。

(2)select_type

解释:
查询的类型:SIMPLE、PRIMARY、SUBQUERY、DERIVED、UNION、UNION RESULT。查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询。

说明:

  • SIMPLE:简单的select查询,查询中不包含子查询或者UNION;
  • PRIMARY:查询中若包含任何复杂的子部分,最外层查询责则被标记为PRIMARY;
  • SUBQUERY:在SELECT或WHERE列表中包含了子查询;
  • DERIVED:在FROM列表中包含的子查询被标记为DERIVED(衍生),MySQL会递归执行这些子查询,把结果放在临时表里;
  • UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被被标记为:DERIVED;
  • UNION RESULT:从UNION表获取结果的SELECT。

(3)table

解释:输出结果集的表。

(4)type

解释:表的访问类型。

说明:
从最好到最差依次是: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

一般来说,得保证查询至少达到range级别,最好能到ref。

  • system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现,这个也可以忽略不计。
  • const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量。
  • eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。
  • ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体。
  • range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等的查询这种范围扫描索引会比全表扫描要好,因为它只需要开始于索引的某一点,而结束于另一点,不用扫描全部索引。
  • index:Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然ALL和index都是全表,但index是从索引出发的,而ALL是从硬盘读取的。)
  • ALL:Full Table Scan,将扫描全表以找到匹配的行。

(5)possible_keys

解释:
显示可能应用在这张表中的索引,一个或多个。
查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。

(6)key

解释:MySQL实际使用的索引。如果为NULL,则没有使用索引。查询中若使用了覆盖索引,则该索引仅出现在key列表中。

(7)key_len

解释:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好。key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len时根据表定义计算而得,不是通过表内检索出的。

计算:

  • 字符串
    • char(n):n字节长度
    • varchar(n):2字节存储字符串长度,如果是utf-8,则长度 3n + 2
  • 数值类型
    • tinyint:1字节
    • smallint:2字节
    • int:4字节
    • bigint:8字节
  • 时间类型
    • date:3字节
    • timestamp:4字节
    • datetime:8字节
  • 如果字段允许为 NULL,需要1字节记录是否为 NULL

索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。

(8)ref

解释:
显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。

(9)rows

解释:
mysql预估要读取并检测的行数,注意这个不是结果集里的行数。

(10)Extra

解释:
包含不适合在其他列中显示但十分重要的额外信息。

说明:

  • using index:这发生在对表的请求列都是同一索引的部分的时候,返回的列数据只使用了索引中的信息,而没有再去访问表中的行记录,是性能高的表现。如果同时出现using where,表明索引被用来执行索引键值的查找;如果没有同时出现using where,表明索引用来读取数据而非执行查找动作。
  • using filesort:MySQL会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。此时MySQL会根据联接类型浏览所有符合条件的记录,并保存排序关键字和行指针,然后排序关键字并按顺序检索行信息。这种情况一般是很危险的,九死一生。
  • using temporary:MySQL需要创建一张临时表来处理查询,对于查询结果排序时使用临时表,常见于排序order by和分组查询group by。常见于这种情况就更加危险了,十死无生。
  • using where:使用where过滤。
  • using join buffer:使用连接缓存。
  • impossible where:where子句总是false,不能用来获取任何元组。
  • select tables optimized away:在没有GROUP BY子句的情况下,基于索引优化MIN/MAX或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。
  • distinct:一旦MySQL找到了与行相联合匹配的行,就停止搜索。

SQL优化

案例1

建表:

  1. CREATE TABLE `tb2_article` (
  2. `id` int NOT NULL AUTO_INCREMENT,
  3. `author_id` int NOT NULL,
  4. `category_id` int NOT NULL,
  5. `views` int NOT NULL,
  6. `comments` int NOT NULL,
  7. `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  8. `content` text NOT NULL,
  9. PRIMARY KEY (`id`)
  10. ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  11. INSERT INTO `tb2_article` VALUES (1, 1, 1, 1, 1, '1', '1');
  12. INSERT INTO `tb2_article` VALUES (2, 2, 2, 2, 2, '2', '2');
  13. INSERT INTO `tb2_article` VALUES (3, 3, 3, 3, 3, '3', '3');

第一次explain:

  1. EXPLAIN SELECT id, author_id FROM tb2_article WHERE category_id = 1 AND comments > 1 order by views desc limit 1;

结果:

  1. +----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+
  4. | 1 | SIMPLE | tb2_article | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50.00 | Using where; Using filesort |
  5. +----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+

建立索引:

  1. CREATE INDEX idx_article_ccv on tb2_article(category_id,comments, views);

第二次explain:

  1. EXPLAIN SELECT id, author_id FROM tb2_article WHERE category_id = 1 AND comments > 1 order by views desc limit 1;

结果:

  1. +----+-------------+-------------+------------+-------+-----------------+-----------------+---------+------+------+----------+---------------------------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-------------+------------+-------+-----------------+-----------------+---------+------+------+----------+---------------------------------------+
  4. | 1 | SIMPLE | tb2_article | NULL | range | idx_article_ccv | idx_article_ccv | 8 | NULL | 1 | 100.00 | Using index condition; Using filesort |
  5. +----+-------------+-------------+------------+-------+-----------------+-----------------+---------+------+------+----------+---------------------------------------+

发现key中已经显示了刚刚建立的索引,但是依然使用了文件排序。

第三次explain:

  1. EXPLAIN SELECT id, author_id FROM tb2_article WHERE category_id = 1 AND comments = 1 order by views desc limit 1;

结果:

  1. +----+-------------+-------------+------------+------+-----------------+-----------------+---------+-------------+------+----------+---------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-------------+------------+------+-----------------+-----------------+---------+-------------+------+----------+---------------------+
  4. | 1 | SIMPLE | tb2_article | NULL | ref | idx_article_ccv | idx_article_ccv | 8 | const,const | 1 | 100.00 | Backward index scan |
  5. +----+-------------+-------------+------------+------+-----------------+-----------------+---------+-------------+------+----------+---------------------+

当把查询条件修改为等于时,发现ref中出现两个常量,即两个查询常量,并且没有使用文件排序。说明当查询条件为大于号时,索引失效。

浅析第二次加了索引之后explain依然使用filesort

按照BTree的工作原理,先排序category_id,如果遇到相同的category_id则再排序comments,如果遇到相同的commnents则再排序views。当comments字段在联合索引里处于中间位置时,因comments > 1条件是一个范围值(range),MySQL无法利用索引再对后面的views部分进行检索,即range类型查询字段后面的索引无效。

删除索引:

  1. DROP INDEX idx_article_ccv ON tb2_article;

新建索引:

  1. CREATE INDEX idx_article_cv ON tb2_article(category_id, views);

再次explain:

  1. EXPLAIN SELECT id, author_id FROM tb2_article WHERE category_id = 1 AND comments > 1 order by views desc limit 1;

结果:

  1. +----+-------------+-------------+------------+------+----------------+----------------+---------+-------+------+----------+----------------------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-------------+------------+------+----------------+----------------+---------+-------+------+----------+----------------------------------+
  4. | 1 | SIMPLE | tb2_article | NULL | ref | idx_article_cv | idx_article_cv | 4 | const | 1 | 50.00 | Using where; Backward index scan |
  5. +----+-------------+-------------+------------+------+----------------+----------------+---------+-------+------+----------+----------------------------------+

可以看到type变成了ref,Extra中的using filesort也消失了,结果非常理想。

结论:建立复合索引的时候最好不要带上含有范围查询的字段。

案例2

继续建表:

  1. CREATE TABLE `tb2_class` (
  2. `id` int NOT NULL AUTO_INCREMENT,
  3. `card` int NOT NULL,
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  6. CREATE TABLE `tb2_book` (
  7. `bookid` int NOT NULL AUTO_INCREMENT,
  8. `card` int NOT NULL,
  9. PRIMARY KEY (`bookid`)
  10. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  11. INSERT INTO tb2_class(card) VALUES(FLOOR(1 + RAND() * 20));
  12. INSERT INTO tb2_book(card) VALUES(FLOOR(1 + RAND() * 20));

第一次explain:

  1. EXPLAIN SELECT * FROM tb2_class LEFT JOIN tb2_book on tb2_class.card = tb2_book.card;

结果:

  1. +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
  4. | 1 | SIMPLE | tb2_class | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | NULL |
  5. | 1 | SIMPLE | tb2_book | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | Using where; Using join buffer (hash join) |
  6. +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+

可以看到type都为ALL。

建立右表tb2_book索引:

  1. ALTER TABLE tb2_book ADD INDEX(card);

第二次explain:

  1. EXPLAIN SELECT * FROM tb2_class LEFT JOIN tb2_book on tb2_class.card = tb2_book.card;

结果:

  1. +----+-------------+-----------+------------+------+---------------+------+---------+----------------------------+------+----------+-------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-----------+------------+------+---------------+------+---------+----------------------------+------+----------+-------------+
  4. | 1 | SIMPLE | tb2_class | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | NULL |
  5. | 1 | SIMPLE | tb2_book | NULL | ref | card | card | 4 | mysql_learn.tb2_class.card | 1 | 100.00 | Using index |
  6. +----+-------------+-----------+------------+------+---------------+------+---------+----------------------------+------+----------+-------------+

可以看到tb2_class的type依然是ALL,tb2_book的type优化为ref。

删除tb2_book的索引:

  1. DROP INDEX card ON tb2_book;

建立左表tb2_class索引:

  1. ALTER TABLE tb2_class ADD INDEX(card);

第三次explain:

  1. EXPLAIN SELECT * FROM tb2_class LEFT JOIN tb2_book on tb2_class.card = tb2_book.card;

结果:

  1. +----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+--------------------------------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+--------------------------------------------+
  4. | 1 | SIMPLE | tb2_class | NULL | index | NULL | card | 4 | NULL | 9 | 100.00 | Using index |
  5. | 1 | SIMPLE | tb2_book | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | Using where; Using join buffer (hash join) |
  6. +----+-------------+-----------+------------+-------+---------------+------+---------+------+------+----------+--------------------------------------------+

可以看到tb2_class的type为index,tb2_book的type下降为ALL。

最后再次建立tb2_book的索引:

  1. ALTER TABLE tb2_book ADD INDEX(card);

第四次explain:

  1. EXPLAIN SELECT * FROM tb2_class LEFT JOIN tb2_book on tb2_class.card = tb2_book.card;

结果:

  1. +----+-------------+-----------+------------+-------+---------------+------+---------+----------------------------+------+----------+-------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-----------+------------+-------+---------------+------+---------+----------------------------+------+----------+-------------+
  4. | 1 | SIMPLE | tb2_class | NULL | index | NULL | card | 4 | NULL | 9 | 100.00 | Using index |
  5. | 1 | SIMPLE | tb2_book | NULL | ref | card | card | 4 | mysql_learn.tb2_class.card | 1 | 100.00 | Using index |
  6. +----+-------------+-----------+------------+-------+---------------+------+---------+----------------------------+------+----------+-------------+

发现type都提升了,index和ref,结果很理想。

结论:对于JOIN连接查询的两张表最好都在联结字段建立单值索引。

案例3

继续建表:

  1. CREATE TABLE `tb2_phone` (
  2. `phoneid` int NOT NULL AUTO_INCREMENT,
  3. `card` int DEFAULT NULL,
  4. PRIMARY KEY (`phoneid`)
  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  6. INSERT INTO tb2_phone(card) VALUES(FLOOR(1 + (RAND()*20)));

第一次explain:

  1. EXPLAIN SELECT * FROM tb2_class LEFT JOIN tb2_book ON tb2_class.card = tb2_book.card LEFT JOIN tb2_phone ON tb2_book.card = tb2_phone.card ;

结果:

  1. +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
  4. | 1 | SIMPLE | tb2_class | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | NULL |
  5. | 1 | SIMPLE | tb2_book | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | Using where; Using join buffer (hash join) |
  6. | 1 | SIMPLE | tb2_phone | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | Using where; Using join buffer (hash join) |
  7. +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+

发现三张表的type都是ALL。

建立tb2_phone和tb2_book的索引:

  1. ALTER TABLE tb2_phone INDEX(card);
  2. ALTER TABLE tb2_book ADD INDEX(card);

第二次explain:

  1. EXPLAIN SELECT * FROM tb2_class LEFT JOIN tb2_book ON tb2_class.card = tb2_book.card LEFT JOIN tb2_phone ON tb2_book.card = tb2_phone.card ;

结果:

  1. +----+-------------+-----------+------------+------+---------------+------+---------+----------------------------+------+----------+-------------+
  2. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
  3. +----+-------------+-----------+------------+------+---------------+------+---------+----------------------------+------+----------+-------------+
  4. | 1 | SIMPLE | tb2_class | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | NULL |
  5. | 1 | SIMPLE | tb2_book | NULL | ref | card | card | 4 | mysql_learn.tb2_class.card | 1 | 100.00 | Using index |
  6. | 1 | SIMPLE | tb2_phone | NULL | ref | card | card | 5 | mysql_learn.tb2_book.card | 1 | 100.00 | Using index |
  7. +----+-------------+-----------+------------+------+---------------+------+---------+----------------------------+------+----------+-------------+

发现tb2_phone和tb2_book的type被优化为ref,并且rows也优化的很好。

结论:

  • 永远用小结果集驱动大结果集
  • 尽可能减少Join语句中的NestedLoop的循环总次数
  • 优先优化NestedLoop的内存循环
  • 保证Join语句中被驱动表上Join条件字段已经被索引
  • 当无法保证被驱动表的Join条件字段被索引且内存资源充足的前提下,不要太吝啬JoinBuffer的设置

索引失效

SQL脚本

  1. CREATE TABLE `tb3_staff` (
  2. `id` int NOT NULL AUTO_INCREMENT,
  3. `name` varchar(20) DEFAULT NULL,
  4. `age` int DEFAULT NULL,
  5. `pos` varchar(20) DEFAULT NULL,
  6. `add_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  7. PRIMARY KEY (`id`)
  8. ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  9. INSERT INTO tb3_staff(name, age, pos, add_time) VALUES ( 'KHighness', 19, 'manager', NOW());
  10. INSERT INTO tb3_staff(name, age, pos, add_time) VALUES ( 'FlowerK', 18, 'dev', NOW());
  11. INSERT INTO tb3_staff(name, age, pos, add_time) VALUES ( 'UnknownK', 17, 'dev', NOW());
  12. ALTER TABLE tb3_staff ADD INDEX id_staff_nameagepos(name, age, pos);

生效场景

  • EXPLAIN SELECT * FROM tb3_staff WHERE name = 'KHighness';
  • EXPLAIN SELECT * FROM tb3_staff WHERE name = 'KHighness' and age = 19;
  • EXPLAIN SELECT * FROM tb3_staff WHERE name = 'KHighness' and age = 19 and pos = "dev";

失效场景

  • EXPLAIN SELECT * FROM tb3_staff WHERE age = 19 and pos = "dev";
  • EXPLAIN SELECT * FROM tb3_staff WHERE pos = "dev";

部分失效

  • EXPLAIN SELECT * FROM tb3_staff WHERE name = 'KHighness' and pos = "dev";

总结

1、最理想的情况就是查询字段与索引字段相同

2、最佳左前缀法则

3、不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描

4、存储引擎不能使用索引中范围条件右边的列

5、尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select *

6、使用不等于(!= 或者 <>)的时候索引失效会导致range(MySQL5中是ALL)

7、使用is null或者is not null的时候索引失效会导致range(MySQL5中是ALL)

8、like以通配符开头索引失效会导致ALL,建立覆盖索引可以防止

9、MySQL5中字符串不加单引号索引失效会导致ALL,MySQL8中直接报错

10、使用or连接索引失效会导致ALL

索引面试

SQL语句

  1. CREATE TABLE `tb4_test` (
  2. `id` int NOT NULL AUTO_INCREMENT,
  3. `c1` char(10) DEFAULT NULL,
  4. `c2` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  5. `c3` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  6. `c4` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  7. `c5` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  8. PRIMARY KEY (`id`)
  9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  10. CREATE INDEX idx_tb4_test_c1234 ON tb4_test(c1, c2, c3, c4);
  11. INSERT INTO `mysql_learn`.`tb4_test`(`id`, `c1`, `c2`, `c3`, `c4`, `c5`) VALUES (1, 'a1', 'a2', 'a3', 'a4', 'a5');
  12. INSERT INTO `mysql_learn`.`tb4_test`(`id`, `c1`, `c2`, `c3`, `c4`, `c5`) VALUES (2, 'b1', 'b2', 'b3', 'b4', 'b5');
  13. INSERT INTO `mysql_learn`.`tb4_test`(`id`, `c1`, `c2`, `c3`, `c4`, `c5`) VALUES (3, 'c1', 'c2', 'c3', 'c4', 'c5');
  14. INSERT INTO `mysql_learn`.`tb4_test`(`id`, `c1`, `c2`, `c3`, `c4`, `c5`) VALUES (4, 'd1', 'd2', 'd3', 'd4', 'd5');
  15. INSERT INTO `mysql_learn`.`tb4_test`(`id`, `c1`, `c2`, `c3`, `c4`, `c5`) VALUES (5, 'e1', 'e2', 'e3', 'e4', 'e5');

EXPLAIN测试

  1. -- 最好索引如何创建就如何使用,避免让MySQL自己再翻译优化 --
  2. -- 1. 用到索引c1 c2 c3 c4全字段,全值匹配
  3. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' AND c3 = 'a3' AND c4 = 'a4';
  4. -- 2. 用到索引c1 c2 c3 c4全字段,查询优化器会优化SQL语句的执行顺序
  5. EXPLAIN SELECT * FROM tb4_test WHERE c4 = 'a4' AND c3 = 'a3' AND c2 = 'a2' AND c1 = 'a1';
  6. -- 3. 用到索引c1 c2 c3字段,c4字段失效,范围之后全失效
  7. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' AND c3 > 'a3' AND c4 = 'a4';
  8. -- 4. 用到索引c1 c2 c3 c4全字段,查询优化器会优化SQL语句的执行顺序
  9. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' AND c4 > 'a4' AND c3 = 'a3';
  10. -- order by排序一定要注意顺序,这个顺序MySQL不会自动优化 --
  11. -- 5. 用到索引c1 c2 c3字段,c1 c2用于查找,c3用于排序,但是没有统计到key_len中,c4字段失效
  12. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' AND c4 > 'a4' ORDER BY c3;
  13. -- 6. 用到索引c1 c2字段,c1 c2用于查找,c4排序产生了Using filesort说明c4失效
  14. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' ORDER BY c4;
  15. -- 7. 用到索引c1 c2 c3字段,c1用于查找,c2 c3用于排序
  16. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c5 = 'a5' ORDER BY c2, c3;
  17. -- 8. 用到索引c1字段,c1用于查找,c3 c2失效,产生了Using filesort
  18. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c5 = 'a5' ORDER BY c3, c2;
  19. -- 9. 用到索引c1 c2 c3字段,c1 c2用于查找,c2 c3用于排序
  20. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' AND c5 = 'a5' ORDER BY c2, c3;
  21. -- 10. 用到索引c1 c2 c3字段,c1 c2用于查找,c3才用于排序
  22. -- 没有产生Using filesort,因为c2查找时已经确定了,排序时c2已经不用排序了
  23. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c2 = 'a2' AND c5 = 'a5' ORDER BY c3, c2;
  24. -- group by虽然是分组,但是分组之前必然排序 --
  25. -- 11. 用到索引c1 c2 c3字段,c1用于查找,c2 c3用于排序,c4失效
  26. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c4 = 'a4' GROUP BY c2, c3;
  27. -- 12. 用到索引c1字段,c1用于查找,c2 c3失效,产生了Using temporary
  28. EXPLAIN SELECT * FROM tb4_test WHERE c1 = 'a1' AND c4 = 'a4' GROUP BY c3, c2;

总结

  • 定值、范围还是排序,一般order by是给个范围。
  • group by基本上都需要进行排序,会有临时表产生。
  • like匹配%在字符串最右边会使用使用,%在字符串最左边不会使用。

一般性建议

  • 对于单键索引,尽量选择针对当前query过滤性更好的索引。
  • 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
  • 在选择组合索引的时候,尽量选择可以能够包含当前query中的where子句中更多字段的索引。
  • 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的。

优化口诀

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

覆盖索引不写星,索引列上少计算;

不等有时会失效,范围之后全失效;

LIKE百分写最右,一般SQL少用OR。