MO_or关于SQL优化的感悟

1、引言

本文是对SQL优化的复习总结,主要记录如何使用索引优化SQL,数据库为MySQL。主要从三个部分依次进行探讨。
第一部分:理解MySQL索引底层数据结构。
第二部分:SQL分析工具Explain详解。
第三部分:MySQL的索引最佳实践。

强烈建议:因为本文篇幅较长,内容较多。推荐读者每次仅阅读一部分,请勿一次性读完(并不利于消化吸取,大佬除外)。html

2、MySQL索引

2.1 索引的简单介绍

索引就是一种帮助MySQL高效获取数据的排好序的数据结构。

2.2 索引的数据结构

这里先说结论,MySQL索引的数据结构是B+TREE。

咱们再依次从二叉树到B+TREE,逐步的理解MySQL索引为何使用B+TREE。

在开始讨论具体的数据结构以前,咱们应该先初步的了解什么是数据结构,数据结构的做用是什么?
若是你们了解设计模式,或者看过个人《MO_or的单例模式复习总结》就能知道。
设计模式是提供了针对不一样类型的问题的优质解决方案。
相对应的,数据结构其实就是提供了针对不一样数据的优质存储方案,
固然这些方案同设计模式同样,也是经过前辈们无数次的实践试错改良所得出的。
那么下面就正式进入几种数据结构的讲解。

2.2.1 二叉树

在了解二叉树以前,咱们须要先明白索引为何要用数据结构?
结合下图,假如咱们在不使用数据结构(图片左侧)状况下,须要读取Col2=89,那么磁盘就须要进行6次I/O。
若是使用二叉树来存储数据(图片右侧),那磁盘仅需进行2此I/O,这就显著的减小了I/O次数,其效率也就相应获得了提高。
由此咱们就能明白为何索引须要使用数据结构。那索引为何不使用二叉树,而要使用B+TREE呢?

二叉树.png

上图的右侧即是一个常见的二叉树模型(模型演示的网址在5、参考)。
二叉树的规则为下一节点左边的元素小于上一节点元素(22<34),
下一节点右边的元素大于等于上一节点元素(89>=34)。
结合下图,咱们就能明白为何索引不使用二叉树。

极端状况二叉树.png

从上图中便能看出,当元素都为依次递增的状况下,二叉树的元素节点则变成了按单列的方式排布。
这时咱们若想取元素6时,那么也只能让磁盘进行6次I/O。
因而为了解决这个问题,咱们就可使用红黑树(平衡二叉树)。

2.2.2 红黑树(平衡二叉树)

直接上图,一样是元素依次递增的状况下。

红黑树.png

能够看出红黑树在基于二叉树的基础上,进行了平衡,再也不是以单列的方式进行排布。
但索引为何依然没有使用红黑树?由于目前数据量较小,层级不高,但数据库中一般会出现几十万、几百万乃至上千万的数据。
咱们能够估算下,若表中存储的数据有100万条,既2^n=100万,n=log(2)(100万),n≈20。
这意味着若咱们须要取得数在最深的节点上,那么就须要读写20次及以上的I/O。
由此咱们能够看出,红黑树在遇到大数据量时,性能依旧较差。并不符合索引能够高效获取数据的这一特色。

2.2.3 B-TREE(多路搜索树)

为了解决红黑树在存储大量数据的状况下,层级依旧很深的问题。因而就有了更好方案,B-TREE。那么咱们先看下B-TREE的模型吧。

B-TREE.png

从上图能够直观的看出,在红黑树的基础上。B-TREE的叶节点(1五、5六、77所相似的行),从原来只能存储一个元素,变为了能够存储多个元素。
这样就大大增长了每一个叶节点的利用空间,减小了层数。但咱们也看到每一个数字节点下方还有个data。
那么新的问题便产生了,这个data即为所存储的数据,那么当一行数据过大时,每一个叶节点所能容纳的元素就相应减小了。
咱们能够经过如下SQL,来查询叶节点的大小,一般为16kb,
SHOW GLOBAL STATUS LIKE 'INNODB_page_size';
若假设一个data为1kb,那意味着每一个叶节点最多能容纳16个元素。那么当数据量过多时上千万,依然存在红黑树同样的问题。

2.2.4 B+TREE

那么终于轮到B+TREE上场了,咱们经过下图一块儿看看B+TREE是如何巧妙地解决B-TREE所面临的问题的吧。

B+TREE.png

能够看出,B+TREE非叶子节点(叶子节点为最下面的一行)是没有存储data的,而是存储索引(冗余)。
只有叶子节点才存储data,而且包含了全部的索引。那么这样作的意义是什么呢?
上面说了叶节点的大小一般为16KB。若索引(冗余)为bigint,再加上空白(链接箭头的起始位置实际上是指针),即8b+6b=14b(估算)。
那么每一个叶节点所能容纳的元素个数:16kb=16*1024b,n=16*1024/14≈1170。那就表示叶子节点大约能够存储1170个元素。
再假设data为1kb,同B-TREE,那就是能够容纳16个元素,那么非叶子节点总共就有:1170*1170*16≈2200万个元素。
一般来讲B+TREE的层次就是2~4层,上千万的数据量也仅需2~4次I/O就能准肯定位。
在大数据量的状况下依旧能高效的获取数据,这即是索引的底层数据结构为B+TREE的缘由。

2.3 最左前缀原理

这里仅简单归纳其原理,具体如何在SQL中体现的,将结合3、Explain的部分进行解读。
当使用联合索引(由多个列组成的索引)时,查询需听从从左到右的顺序,且不能跳过中间的列。不然会致使索引失效。

3、Explain

3.1 Explain详解

在上一部分中,咱们对索引有了较为深刻的理解了,但并不要着急,这一部分暂时还不会详细的探讨如何利用索引优化SQL。
在此以前,咱们还须要了解分析SQL性能的一个工具,即Explain。
使用Explain关键字,能够模拟优化器执行SQL语句,分析查询语句或结构的性能瓶颈。咱们能够根据分析结果,进行对应的优化。

那么如今结合SQL咱们来看看Explain吧。
-- 演员表
DROP TABLE IF EXISTS `actor`;
CREATE TABLE `actor`  (
  `id` int(11) NOT NULL,
  `name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `update_time` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `actor` VALUES 
(1, 'a', '2018-10-23 16:04:40'),
(2, 'b', '2018-10-23 16:04:40'),
(3, 'c', '2018-10-23 16:04:40');

-- 电影表
DROP TABLE IF EXISTS `film`;
CREATE TABLE `film`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `film` VALUES 
(1, 'film1'),
(2, 'film2'),
(3, 'film0');

-- 电影演员关系表
DROP TABLE IF EXISTS `film_actor`;
CREATE TABLE `film_actor`  (
  `id` int(11) NOT NULL,
  `film_id` int(11) NOT NULL,
  `actor_id` int(11) NOT NULL,
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_film_actor_id`(`film_id`, `actor_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `film_actor` VALUES 
(1, 1, 1, NULL),
(2, 1, 2, NULL),
(3, 2, 1, NULL);
mysql> EXPLAIN SELECT * FROM actor;

image.png

上图为执行EXPLAIN展现的结果,如有join链接多个表时,则每join一个表多输出一行。
其中type列是须要较长时间理解的列,固然随着使用次数增多天然而然也会熟能生巧,因此并不须要死记硬背。

3.1.1 id列

1)id编号就是select的序列号,有几个select就有几个id,而且id的顺序是按select出现顺序而增加的。
2)id越大执行优先级越高,id相同则从上至下执行,id为null则最后执行。

3.1.2 select-type列

select-type列表示简单仍是复杂查询,共有如下类型:
1)simple:简单查询,不包含子查询subquery、derived和union。
2)primary:复杂查询最外层的select。
3)subquery:select后的子查询(不包含from后)
4)derived:from后的子查询,MySQL会将查询结果存入临时表,也称派生表。咱们经过SQL来看一下:
EXPLAIN SELECT
    ( SELECT 1 FROM actor WHERE id = 1 ) 
FROM
    ( SELECT * FROM film WHERE id = 1 ) der;

image.png

5)union:在union中的第二个和随后的select。

3.1.3 table列

该列表示explain的一行正在访问哪一个表。
当from中有子查询时,该列展现为<derivedN>,N表示id编号,意味着先执行id=N的查询。
当有union时,UNION RESULT的table列的值为 <union1,2>,1和2表示参与 union 的select 行id。

3.1.4 type列

这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL。
通常来讲,得保证查询达到range级别,最好达到ref。
NULL:mysql可以在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。
例如:在索引列中选取最小值,能够单独查找索引来完成,不须要在执行时访问表 

const, system:
mysql能对查询的某部分进行优化并将其转化成一个常量(能够看showwarnings 的结果)。
用于 primary key 或 unique key 的全部列与常数比较时,因此表最多有一个匹配行,读取1次,速度比较快。
system是const的特例,表里只有一条元组匹配时为system。

eq_ref:
primary key 或 unique key 索引的全部部分被链接使用 ,最多只会返回一条符合条件的记录。
这多是在 const 以外最好的联接类型了,简单的 select 查询不会出现这种type。

ref:相比 eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前缀,
索引要和某个值相比较,可能会找到多个符合条件的行。

range:范围扫描一般出如今 in(), between ,> ,<, >= 等操做中。使用一个索引来检索给定范围的行。

index:扫描全表索引,这一般比ALL快一些。

ALL:即全表扫描,意味着mysql须要从头至尾去查找所须要的行。一般状况下这须要增长索引来进行优化了。

3.1.5 possible_keys列

该列表示可能使用到的索引列。
explain时可能出现possible-keys列有值,key列为null。可能时由于数据量较少,mysql认为全表扫描效率更高。
若该列为null,则没有相关索引。此时可考虑增长适当索引来提升查询效率。

3.1.6 key列

该列表示mysql实际使用的索引。
若没有使用索引,则该列为null。
若是想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。

3.1.7 key_len列

该列表示mysql使用索引的字节数,在使用联合索引时经过key_len就能知道具体使用了那些列。
好比下面的SQL,key_len=4,就能够推断出仅用了联合索引中的id列,由于int占4个字节。
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id = 2;

image.png

key_len计算规则以下:
1)字符串
    char(n):n字节长度
    varchar(n):2字节存储字符串长度,若是是utf-8,则长度3n+2
2)数值类型
    tinyint:1字节
    smallint:2字节
    int:4字节
    bigint:8字节
3)时间类型
    date:3字节
    timestamp:4字节
    datetime:8字节
若是字段容许为 NULL,须要1字节记录是否为 NULL

3.1.8 ref列

这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)。

3.1.9 rows列

这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。

3.1.10 Extra列

这一列展现的是额外信息。常见的重要值以下:
1)Using index:使用覆盖索引。
2)Using where:使用 where 语句来处理结果,查询的列未被索引覆盖。
3)Using index condition:查询的列不彻底被索引覆盖,where条件中是一个前导列的范围。
4)Using temporary:mysql须要建立一张临时表来处理查询。出现这种状况通常是要进行优化的,首先是想到用索引来优化。
5)Using filesort:将用外部排序而不是索引排序,数据较小时从内存排序,不然须要在磁盘完成排序。这种状况下通常也是要考虑使用索引来优化的。
6)Select tables optimized away:使用某些聚合函数(好比 max、min)来访问存在索引的某个字段是。

4、索引最佳实践

-- 员工表
CREATE TABLE `employees` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`name` VARCHAR ( 24 ) NOT NULL DEFAULT '' COMMENT '姓名',
`age` INT ( 11 ) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` VARCHAR ( 20 ) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时 间',
PRIMARY KEY ( `id` ),
KEY `idx_name_age_position` ( `name`, `age`, `position` ) USING BTREE 
) ENGINE = INNODB AUTO_INCREMENT = 4 DEFAULT CHARSET = utf8 COMMENT = '员工记录表';

INSERT INTO employees ( NAME, age, position, hire_time )
VALUES
    ( 'LiLei', 22, 'manager', NOW( ) ),
    ( 'HanMeimei', 23, 'dev', NOW( ) ),
    ( 'Lucy', 23, 'dev', NOW( ) );

4.1全值匹配

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei';

image.png

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22;

image.png

EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';

image.png

4.2.最左前缀法则

若是索引了多列,要遵照最左前缀法则。指的是查询从索引的最左前列开始而且不跳过索引中的列。

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

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

EXPLAIN SELECT \* FROM employees WHERE name= 'LiLei' AND age > 22 AND position ='manager';

image.png

4.5.尽可能使用覆盖索引(只访问索引的查询(索引列包含查询列)),减小select *语句

4.6.mysql在使用不等于(!=或者<>)的时候没法使用索引会致使全表扫描

4.7.is null,is not null 也没法使用索引

4.8.like以通配符开头('$abc...')mysql索引失效会变成全表扫描操做

EXPLAIN SELECT * FROM employees WHERE name like '%Lei'

image.png

4.9.字符串不加单引号索引失效

4.10.少用or或in,用它查询时,mysql不必定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素总体评估是否使用索引,详见范围查询优化

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';

image.png

4.11.范围查询优化

给年龄添加单值索引。
ALTER TABLE `employees` ADD INDEX `idx_age` ( `age` ) USING BTREE;
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 100;

image.png

没走索引缘由:mysql内部优化器会根据检索比例、表大小等多个因素总体评估是否使用索引。
好比这个例子,多是因为单次数据量查询过大致使优化器最终选择不走索引。
优化方法:能够讲大的范围拆分红多个小范围。
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 50;
EXPLAIN SELECT * FROM employees WHERE age >= 51 AND age <= 100;

image.png


以上所有代码均已在本机执行且无误。mysql

5、参考

数据结构动态演示模型sql

MO_or的单例模式复习总结数据库

书写高质量SQL的30条建议segmentfault

6、最后

如有不足,敬请指正。
求知若渴,虚心若愚。
相关文章
相关标签/搜索