文章目录
- 一、SQL执行顺序
- 二、索引简介
- 三、Explain简介
- 四、Explain 详解
- 五、索引优化
- 六、索引失效
-
-
- 1、带头大哥不能死,中间兄弟不能断
- 2、不在索引列上做任何操作(函数、计算、类型转换等操作)
- 3、范围之后的索引字段全失效
- 4、索引列和查询列尽量一致,减少`select *`
- 5、mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描
- 6、is null,is not null 也无法使用索引
- 7、 like操作时%开头会导致索引失效,`like '%abc%'`还有`like '%abc'`会导致索引失效,但是`like 'abc%'`不会导致索引失效
- 8、解决【like ‘%str%’ 】索引失效的问题——覆盖索引
- 9、字符串不加单引号索引失效
- 10、少用or,用它连接时会索引失效
- 11、索引优化面试题
- 12、索引失效总结
-
- 七、in和Exists语句怎么使用性能好
- 八、ORDER BY 优化
一、SQL执行顺序
我们手写的 SQL 顺序
MySQL 实际执行 SQL 顺序
二、索引简介
1、关于索引
- 单值索引:给一个字段添加索引。在user表中给name属性建个索引,
create index idx_user_name on user(name)
- 复合索引:给几个字段添加索引。在user表中给name、email属性建个索引,
create index idx_user_nameEmail on user(name,email)
索引的本质:
索引就是排序,你对某个字段建索引数据库就会对所有数据针对这个字段做一个排序。站队从高到低站好找人就很好找,如果没有索引那就是全表扫描。
建索引时注意点:
由于肯定有一张表要全表扫描,被全表扫描的这个表最好是数据量很小的表。
务必小表驱动大表,然后在大表上面建索引
哪些情况下适合建立索引
- 主键自动建立唯一索引
- 频繁作为查询的条件的字段应该创建索引
- 查询中与其他表关联的字段,外键关系建立索引
- 频繁更新的字段不适合创建索引
- Where 条件里用不到的字段不创建索引
- 单间/组合索引的选择问题,Who?(在高并发下倾向创建组合索引)
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序的速度
- 查询中统计或者分组字段
哪些情况不要创建索引
- 表记录太少
- 经常增删改的表
- 数据重复且分布平均的表字段,如性别只有男女两种结果,建了索引也没有意义。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99,一个索引的选择性越接近于1,这个索引的效率就越高。
2、索引的类型
Btree 索引
Btree 索引搜索过程
【初始化介绍】
- 一颗 b 树, 浅蓝色的块我们称之为一个磁盘块, 可以看到每个磁盘块包含几个数据项(深蓝色所示) 和指针(黄色所示)
- 如磁盘块 1 包含数据项 17 和 35, 包含指针 P1、 P2、 P3
- P1 表示小于 17 的磁盘块, P2 表示在 17 和 35 之间的磁盘块, P3 表示大于 35 的磁盘块
- 真实的数据存在于叶子节点和非叶子节点中
【查找过程】
- 如果要查找数据项 29, 那么首先会把磁盘块 1 由磁盘加载到内存, 此时发生一次 IO, 在内存中用二分查找确定 29在 17 和 35 之间, 锁定磁盘块 1 的 P2 指针, 内存时间因为非常短(相比磁盘的 IO) 可以忽略不计
- 通过磁盘块 1的 P2 指针的磁盘地址把磁盘块 3 由磁盘加载到内存, 发生第二次 IO, 29 在 26 和 30 之间, 锁定磁盘块 3 的 P2 指针
- 通过指针加载磁盘块 8 到内存, 发生第三次 IO, 同时内存中做二分查找找到 29, 结束查询, 总计三次 IO。
B+tree 索引
B+tree 索引搜索过程
【B+Tree 与 BTree 的区别】
B-树的关键字(数据项)和记录是放在一起的; B+树的非叶子节点中只有关键字和指向下一个节点的索引, 记录只放在叶子节点中。
【B+Tree 与 BTree 的查找过程】
- 在 B 树中, 越靠近根节点的记录查找时间越快, 只要找到关键字即可确定记录的存在; 而 B+ 树中每个记录的查找时间基本是一样的, 都需要从根节点走到叶子节点, 而且在叶子节点中还要再比较关键字。
- 从这个角度看 B 树的性能好像要比 B+ 树好, 而在实际应用中却是 B+ 树的性能要好些。 因为 B+ 树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比 B 树多, 树高比 B 树小, 这样带来的好处是减少磁盘访问次数。
- 尽管 B+ 树找到一个记录所需的比较次数要比 B 树多, 但是一次磁盘访问的时间相当于成百上千次内存比较的时间, 因此实际中B+ 树的性能可能还会好些, 而且 B+树的叶子节点使用指针连接在一起, 方便顺序遍历(范围搜索), 这也是很多数据库和文件系统使用 B+树的缘故。
【性能提升】
真实的情况是, 3 层的 B+ 树可以表示上百万的数据, 如果上百万的数据查找只需要三次 IO, 性能提高将是巨大的,如果没有索引, 每个数据项都要发生一次 IO, 那么总共需要百万次的 IO, 显然成本非常非常高。
【思考: 为什么说 B+树比 B-树更适合实际应用中操作系统的文件索引和数据库索引?】
- B+树的磁盘读写代价更低:B+树的内部结点并没有指向关键字具体信息的指针。 因此其内部结点相对 B 树更小。 如果把所有同一内部结点的关键字存放在同一盘块中, 那么盘块所能容纳的关键字数量也越多。 一次性读入内存中的需要查找的关键字也就越多。 相对来说 IO 读写次数也就降低了。
- B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点, 而只是叶子结点中关键字的索引。 所以任何关键字的查找必须走一条从根结点到叶子结点的路。 所有关键字查询的路径长度相同, 导致每一个数据的查询效率相当。
三、Explain简介
Explain能干嘛?
- 能看到一条sql语句实际执行时的MySQL底层的加载顺序(根据输出的id 字段去看)
- 数据读取操作的操作类型(select_type 字段)
- 哪些索引可能被使用(possible_keys 字段)
- 哪些索引被实际使用(keys 字段)
- 表之间的引用(ref 字段)
- 每张表有多少行被优化器查询(rows 字段)
Explain的用法:
- Explain + SQL语句
mysql> explain select * from tbl_emp;
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | tbl_emp | ALL | NULL | NULL | NULL | NULL | 8 | NULL |
+----+-------------+---------+------+---------------+------+---------+------+------+-------+
1 row in set (0.00 sec)
四、Explain 详解
1、id
id:根据id字段你可以看到这条sql实际执行时的MySQL底层的加载顺序
id 取值的三种情况:
- id相同,执行顺序由上至下
可以看到MySQL底层是先读取t1,然后读取t3,最后读取的t2
- id不同,id值越大优先级越高,越先被执行
可以看到MySQL底层是先读取t3,然后读取t1,最后读取的t2
- id既有相同的也有不同的:id相同时,可以认为是一组,从上往下顺序执行;id不同时,id值越大,优先级越高,越先执行;
可以看到MySQL底层是先读取t3,然后读取
<DERIVED2>
,最后读取的t2。
DERIVED是衍生的意思,<DERIVED2>
中的2是指基于id=2的查询结果做衍生,下图中<DERIVED2>
的含义就是基于t3表的查询结果继续执行查询语句
2、select_type
select_type:查询的类型,主要用于区别普通查询、联合查询、子查询等复杂查询
- SIMPLE:简单的select查询,查询中不包含子查询或者UNION
- PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为PRIMARY。
- SUBQUERY:在SELECT或者WHERE列表中包含了子查询
- DERIVED:在FROM列表中包含的子查询被标记为DERIVED(衍生)MySQL会递归执行这些子查询,把结果放在临时表里
- UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
- UNION RESULT:从UNION表获取结果的SELECT
UNION 和 UNION RESULT举例
explain
-> select * from tbl_emp e left join tbl_dept d on e.deptId = d.id
-> union
-> select * from tbl_emp e right join tbl_dept d on e.deptId = d.id;
+----+--------------+------------+------+---------------+------------+---------+-----------+------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+------------+------+---------------+------------+---------+-----------+------+----------------------------------------------------+
| 1 | PRIMARY | e | ALL | NULL | NULL | NULL | NULL | 8 | NULL |
| 1 | PRIMARY | d | ALL | PRIMARY | NULL | NULL | NULL | 5 | Using where; Using join buffer (Block Nested Loop) |
| 2 | UNION | d | ALL | NULL | NULL | NULL | NULL | 5 | NULL |
| 2 | UNION | e | ref | fk_dept_Id | fk_dept_Id | 5 | db01.d.id | 1 | NULL |
| NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------+---------------+------------+---------+-----------+------+----------------------------------------------------+
5 rows in set (0.00 sec)
3、table
table:显示这一行的数据是关于哪张表的
4、type
type:访问类型排列,显示查询使用了何种类型
- type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是:
system>const>eq_ref>ref>fultext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>ALL
- 挑重要的来说:
system>const>eq_ref>ref>range>index>ALL
,一般来说,得保证查询至少达到range级别,最好能达到ref。
从最好到最差依次是:system>const>eq_ref>ref>range>index>ALL
-
system:一张表中只有一行记录才会出现的情况。那么这种表类似于系统表一样只有一条记录,平时基本不会出现。
-
const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量
- eq_ref:唯一性索引,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
下图中的sql语句执行时先全表扫描了t2,然后以t2的数据为基石去匹配t1的数据,匹配t1的数据时因为是一对一的关系,所以就是eq_ref
- ref:非唯一索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体
找t1表中col1为ac的数据,由于是一对多,所以type就是ref
- range:在你的where语句中出现了
between
、<
、>
、in
等的查询这种范围扫描索就是range类型,因为他只需要开始索引的某一点,而结束于另一点,不用扫描全部数据
- index:Full Index Scan,index与ALL区别为index类型是全索引扫描,而all是全表扫描。index通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘数据库文件中读的)
- all:将遍历全表以找到匹配的行(全表扫描)
- 备注:一般来说,得保证查询只是达到range级别,最好达到ref
5、possible_keys
possible_keys显示可能会用到的索引(若查询涉及的字段上存在索引,则该索引将被列出,但不一定被查询实际使用)
好比是大摆筵席,possible_keys就是你可能会摆多少桌,但实际有多少桌这个看的是key
6、key
-
实际使用的索引,如果为null,则没有使用索引
-
若查询中使用了覆盖索引,则该索引仅出现在key列表中
下图就很有意思,程序判断可能用到的索引是空,结果实际用到的索引是idx_col1_col2,这种情况就是索引覆盖,这是好事情,我本来以为么有索引可以使用结果有。
7、key_len
key_len显示的值为索引最大可能长度,一般来讲长度越短越好,但是随着你查询条件的变多,key_len会不可避免的变长。(我找找山西里面的李华,我要找山西太原的李华,我要找山西太原小店区的李华,由于你找的越来越精确,所以key_len会不可避免的变长,这是正常情况)
8、ref
- 显示索引哪一列被使用了,如果可能的话,最好是一个常数。哪些列或常量被用于查找索引列上的值
- 由key_len可知t1表的索引idx_col1_col2被充分使用,t1表的col1匹配t2表的col1,t1表的col2匹配了一个常量,即’ac’
下图的sql就是说我先全表扫描了t2,然后以t2的数据为基石,查找t1表中t1.col1 = t2.col1 而且 t1.col2为ac的数据。
ref中的shared.t2.col1
是啥意思?它表示t1表的col1用到了shared数据库下t2表的col1字段。
ref中的const
是啥意思?它表示t1表的col2匹配了一个常量,即’ac’。
也就是说查找t1表中t1.col1 = t2.col1 而且 t1.col2为ac的数据时既用到shared数据库下t2表的col1字段 还用到了一个常量。
9、rows
根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数
10、Extra
Extra包含不适合在其他列中显示但十分重要的额外信息,这个是重点,一定不能忽视!!!
Extra可能有以下几个值:
-
Using filesort(文件排序):
- 出现 Using filesort 不好(九死一生),需要尽快优化 SQL
- MySQL中无法利用索引完成排序操作成为“文件排序”
- 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取
你定义了一个索引,然后MySQL底层按照你的索引给你排好序了,结果你不仅没有用上这个索引,你还和索引规定的东西反着来,那么就会出现 Using filesort ,这个非常不好,需要尽快优化 SQL。举例:你基于col1,col2,col3做了一个索引,然后你select查询时要求按照col1的升序、col2的降序、col3的升序排序,MySQL底层的B+树是按照col1,col2,col3的升序排队的,结果你查询语句是按照col1的升序、col2的降序
、col3的升序排序,你的要求有升又有降,和底层不符合,那么就会出现Using filesort 。
下图中\G表示竖着展示(以前都是横着展示指标)。你基于col1,col2,col3做了一个索引,示例中第一个查询只使用了 col1然后按照col3做了排序,原有索引派不上用场,所以进行了外部文件排序。示例中第二个查询使用了 col1、col2 和col3,原有索引派上用场,无需进行文件排序
-
Using temporary(创建临时表):
- 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by
- 出现 Using temporary 超级不好(十死无生),需要立即优化 SQL
你基于col1,col2,col3做了一个索引,示例中第一个查询只使用了col1,原有索引派不上用场,所以创建了临时表进行分组;示例中第二个查询使用了 col1、col2,原有索引派上用场,无需创建临时表
-
Using index(覆盖索引):
-
表示相应的select操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!
-
如果同时出现using where,表明索引被用来执行索引键值的查找
-
如果没有同时出现using where,表明索引用来读取数据而非执行查找动作
-
覆盖索引(Covering Index),也说为索引覆盖
- 理解方式一:就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。
- 理解方式二:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了(或覆盖了)满足查询结果的数据就叫做覆盖索引。
- 注意:如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可以用
select *
,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降。
-
-
Using where:表明使用了where过滤
-
Using join buffer:表明使用了连接缓存
-
impossible where:where子句的值总是false,不能用来获取任何元组
-
select tables optimized away:在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化
COUNT(*)
操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。 -
distinct:优化distinct,在找到第一匹配的元组后即停止找同样值的工作
11、小案例
- 第一行(id为1,但它的执行顺序是4):id列为1,select_type列的primary表示该查询为外层查询,table列被标记为
<derived3>
,表示查询结果来自一个衍生表,其中derived3中3代表该查询衍生自id为3的select查询。【select d1.name ...
】 - 第二行(id为3,但执行顺序2):id为3,是整个查询中第三个select的一部分。因查询包含在from中,所以为derived。【
select id, name from t1 where other_column= ' '
】 - 第三行(id为2,但执行顺序3):select列表中的子查询select_type为subquery,为整个查询中的第二个select。【
select id from t3
】 - 第四行(id为4,但执行顺序1):select_type为union,说明第四个select是union里的第二个select,最先执行【
select name, id from t2
】 - 第五行(id为null,执行顺序5):代表从union的临时表中读取行的阶段,table列的<union1, 4>表示用第一个和第四个select的结果进行union操作。【两个结果进行uinion操作】
五、索引优化
1、单表索引优化
单表索引优化分析
创建表
- 建表 SQL
CREATE TABLE IF NOT EXISTS article(
id INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
author_id INT(10) UNSIGNED NOT NULL,
category_id INT(10) UNSIGNED NOT NULL,
views INT(10) UNSIGNED NOT NULL,
comments INT(10) UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL
);
INSERT INTO article(author_id,category_id,views,comments,title,content)
VALUES
(1,1,1,1,'1','1'),
(2,2,2,2,'2','2'),
(1,1,3,3,'3','3');
- 表中的测试数据
mysql> SELECT * FROM article;
+----+-----------+-------------+-------+----------+-------+---------+
| id | author_id | category_id | views | comments | title | content |
+----+-----------+-------------+-------+----------+-------+---------+
| 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 2 | 2 | 2 | 2 |
| 3 | 1 | 1 | 3 | 3 | 3 | 3 |
+----+-----------+-------------+-------+----------+-------+---------+
3 rows in set (0.00 sec)
查询案例
- 查询category_id为1且comments 大于1的情况下,views最多的article_id。
mysql> SELECT id, author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
+----+-----------+
| id | author_id |
+----+-----------+
| 3 | 1 |
+----+-----------+
1 row in set (0.00 sec)
- 此时 article 表中只有一个主键索引
mysql> SHOW INDEX FROM article;
+---------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| article | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
+---------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set (0.00 sec)
1234567
- 使用 explain 分析 SQL 语句的执行效率:
EXPLAIN SELECT id, author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
mysql> EXPLAIN SELECT id, author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
+----+-------------+---------+------+---------------+------+---------+------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+------+---------+------+------+-----------------------------+
| 1 | SIMPLE | article | ALL | NULL | NULL | NULL | NULL | 3 | Using where; Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+-----------------------------+
1 row in set (0.00 sec)
- 结论:
- 很显然,type是ALL,即最坏的情况。
- Extra 里还出现了Using filesort,也是最坏的情况。
- 优化是必须的。
开始优化:新建索引
- 创建索引的 SQL 命令
# ALTER TABLE article ADD INDEX idx_article_ccv('category_id', 'comments', 'views');
create index idx_article_ccv on article(category_id, comments, views);
- 在 category_id 列、comments 列和 views 列上建立联合索引
mysql> create index idx_article_ccv on article(category_id, comments, views);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW INDEX FROM article;
+---------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| article | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_ccv | 1 | category_id | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_ccv | 2 | comments | A | 3 | NULL | NULL | | BTREE | | |
| article | 1 | idx_article_ccv | 3 | views | A | 3 | NULL | NULL | | BTREE | | |
+---------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
4 rows in set (0.00 sec)
- 再次执行查询:type变成了range,这是可以忍受的。但是extra里使用Using filesort仍是无法接受的。
mysql> EXPLAIN SELECT id, author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
+----+-------------+---------+-------+-----------------+-----------------+---------+------+------+---------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+------+---------------------------------------+
| 1 | SIMPLE | article | range | idx_article_ccv | idx_article_ccv | 8 | NULL | 1 | Using index condition; Using filesort |
+----+-------------+---------+-------+-----------------+-----------------+---------+------+------+------------------