本文是对SQL优化的复习总结,主要记录如何使用索引优化SQL,数据库为MySQL。主要从三个部分依次进行探讨。 第一部分:理解MySQL索引底层数据结构。 第二部分:SQL分析工具Explain详解。 第三部分:MySQL的索引最佳实践。
强烈建议:因为本文篇幅较长,内容较多。推荐读者每次仅阅读一部分,请勿一次性读完(并不利于消化吸取,大佬除外)。html
索引就是一种帮助MySQL高效获取数据的排好序的数据结构。
这里先说结论,MySQL索引的数据结构是B+TREE。 咱们再依次从二叉树到B+TREE,逐步的理解MySQL索引为何使用B+TREE。 在开始讨论具体的数据结构以前,咱们应该先初步的了解什么是数据结构,数据结构的做用是什么? 若是你们了解设计模式,或者看过个人《MO_or的单例模式复习总结》就能知道。 设计模式是提供了针对不一样类型的问题的优质解决方案。 相对应的,数据结构其实就是提供了针对不一样数据的优质存储方案, 固然这些方案同设计模式同样,也是经过前辈们无数次的实践试错改良所得出的。 那么下面就正式进入几种数据结构的讲解。
在了解二叉树以前,咱们须要先明白索引为何要用数据结构? 结合下图,假如咱们在不使用数据结构(图片左侧)状况下,须要读取Col2=89,那么磁盘就须要进行6次I/O。 若是使用二叉树来存储数据(图片右侧),那磁盘仅需进行2此I/O,这就显著的减小了I/O次数,其效率也就相应获得了提高。 由此咱们就能明白为何索引须要使用数据结构。那索引为何不使用二叉树,而要使用B+TREE呢?
上图的右侧即是一个常见的二叉树模型(模型演示的网址在5、参考)。 二叉树的规则为下一节点左边的元素小于上一节点元素(22<34), 下一节点右边的元素大于等于上一节点元素(89>=34)。 结合下图,咱们就能明白为何索引不使用二叉树。
从上图中便能看出,当元素都为依次递增的状况下,二叉树的元素节点则变成了按单列的方式排布。 这时咱们若想取元素6时,那么也只能让磁盘进行6次I/O。 因而为了解决这个问题,咱们就可使用红黑树(平衡二叉树)。
直接上图,一样是元素依次递增的状况下。
能够看出红黑树在基于二叉树的基础上,进行了平衡,再也不是以单列的方式进行排布。 但索引为何依然没有使用红黑树?由于目前数据量较小,层级不高,但数据库中一般会出现几十万、几百万乃至上千万的数据。 咱们能够估算下,若表中存储的数据有100万条,既2^n=100万,n=log(2)(100万),n≈20。 这意味着若咱们须要取得数在最深的节点上,那么就须要读写20次及以上的I/O。 由此咱们能够看出,红黑树在遇到大数据量时,性能依旧较差。并不符合索引能够高效获取数据的这一特色。
为了解决红黑树在存储大量数据的状况下,层级依旧很深的问题。因而就有了更好方案,B-TREE。那么咱们先看下B-TREE的模型吧。
从上图能够直观的看出,在红黑树的基础上。B-TREE的叶节点(1五、5六、77所相似的行),从原来只能存储一个元素,变为了能够存储多个元素。 这样就大大增长了每一个叶节点的利用空间,减小了层数。但咱们也看到每一个数字节点下方还有个data。 那么新的问题便产生了,这个data即为所存储的数据,那么当一行数据过大时,每一个叶节点所能容纳的元素就相应减小了。 咱们能够经过如下SQL,来查询叶节点的大小,一般为16kb, SHOW GLOBAL STATUS LIKE 'INNODB_page_size'; 若假设一个data为1kb,那意味着每一个叶节点最多能容纳16个元素。那么当数据量过多时上千万,依然存在红黑树同样的问题。
那么终于轮到B+TREE上场了,咱们经过下图一块儿看看B+TREE是如何巧妙地解决B-TREE所面临的问题的吧。
能够看出,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的缘由。
这里仅简单归纳其原理,具体如何在SQL中体现的,将结合3、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;
上图为执行EXPLAIN展现的结果,如有join链接多个表时,则每join一个表多输出一行。 其中type列是须要较长时间理解的列,固然随着使用次数增多天然而然也会熟能生巧,因此并不须要死记硬背。
1)id编号就是select的序列号,有几个select就有几个id,而且id的顺序是按select出现顺序而增加的。 2)id越大执行优先级越高,id相同则从上至下执行,id为null则最后执行。
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;
5)union:在union中的第二个和随后的select。
该列表示explain的一行正在访问哪一个表。 当from中有子查询时,该列展现为<derivedN>,N表示id编号,意味着先执行id=N的查询。 当有union时,UNION RESULT的table列的值为 <union1,2>,1和2表示参与 union 的select 行id。
这一列表示关联类型或访问类型,即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须要从头至尾去查找所须要的行。一般状况下这须要增长索引来进行优化了。
该列表示可能使用到的索引列。 explain时可能出现possible-keys列有值,key列为null。可能时由于数据量较少,mysql认为全表扫描效率更高。 若该列为null,则没有相关索引。此时可考虑增长适当索引来提升查询效率。
该列表示mysql实际使用的索引。 若没有使用索引,则该列为null。 若是想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。
该列表示mysql使用索引的字节数,在使用联合索引时经过key_len就能知道具体使用了那些列。 好比下面的SQL,key_len=4,就能够推断出仅用了联合索引中的id列,由于int占4个字节。
mysql> EXPLAIN SELECT * FROM film_actor WHERE film_id = 2;
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
这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)。
这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。
这一列展现的是额外信息。常见的重要值以下: 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)来访问存在索引的某个字段是。
-- 员工表 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( ) );
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei';
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22;
EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';
若是索引了多列,要遵照最左前缀法则。指的是查询从索引的最左前列开始而且不跳过索引中的列。
EXPLAIN SELECT \* FROM employees WHERE name= 'LiLei' AND age > 22 AND position ='manager';
EXPLAIN SELECT * FROM employees WHERE name like '%Lei'
EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';
给年龄添加单值索引。
ALTER TABLE `employees` ADD INDEX `idx_age` ( `age` ) USING BTREE;
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 100;
没走索引缘由:mysql内部优化器会根据检索比例、表大小等多个因素总体评估是否使用索引。 好比这个例子,多是因为单次数据量查询过大致使优化器最终选择不走索引。 优化方法:能够讲大的范围拆分红多个小范围。
EXPLAIN SELECT * FROM employees WHERE age >= 1 AND age <= 50;
EXPLAIN SELECT * FROM employees WHERE age >= 51 AND age <= 100;
以上所有代码均已在本机执行且无误。mysql
数据结构动态演示模型sql
书写高质量SQL的30条建议segmentfault
如有不足,敬请指正。 求知若渴,虚心若愚。