1.索引
按照索引的重要性分为主键索引,唯一索引,普通索引
- 主键索引:不能为空,不能重复。
- 唯一索引:可以为空,但是不能重复。
- 普通索引:可以为空,也可以重复。
按照索引的列数目分为单列索引和联合索引
- 单列索引:在一列上设置索引。
- 联合索引:在多列上设置索引。
按照索引的物理存储结构分为聚簇索引和非聚簇索引(也叫辅助索引),它们的区别下面会介绍
索引的类型分为B+Tree和Hash,一般我们会使用B+Tree的方式,因为Hash使用的是哈希表的方式建立的索引,不适合范围查找。
2.索引的结构
我们知道索引可以提高数据查询的效率,但是因为什么?这就需要说明索引的结构
①二叉树:我们为了提高数据的查询效率想到了使用二叉树来实现折半查找。
如左图,使用二叉树查找了可以实现log2(N)的查找效率,比如,当查找18时,使用顺序查找需要2,5,7...18共6次,而通过二叉树只需要3次足够。但是如果二叉树建立不好,就会出现像右边那样的情况,那种情况二叉树完全变成了链表,查询效率也和顺序查找一样了。
②平衡二叉树:为了得到比较好且稳定的查询效率,我们有设计了平衡二叉树(avl)。这种树其实就是让树旋转,从而让树的左右两边高度相差不大于1。
③B树(多路平衡搜索树):avl树虽然具有稳定的查询效率,但是查询效率不高,遍历的过程中每个节点只能剔除一半的数据,所以出现了多路平衡搜索树(B树)。avl树与红黑树avl树与红黑树的区别:红黑树也是一种平衡二叉树,但是它是一种弱平衡的二叉树。AVL树是严格的平衡二叉树,所有节点的左右子树高度差的绝对值不超过1;红黑树则要求最长路径不超过最短路径的2倍。所以红黑树插入和删除节点的旋转代价比较小。
所以AVL树适合用于插入与删除次数比较少,但查找多的情况,HashMap的底层就是用红黑树来实现的,而索引则使用查询效率较高的avl树的变体B+树.
④B+树:数据库是按页进行读取的(一次读取16k,磁盘块大小为512B),B树的设计结构将关键字和数据放在了一起,这样每次将关键字从磁盘读到内存时都需要将关键字连带的数据读出来,大大降低了读取的效率。
- B+树将关键字和数据分开存放,这样一页就可以存放更多的关键字。而不需要读取无用的行数据。
- B+树将行数据存放在叶子节点中,非叶子节点只作为比较。所以每个节点的左子节点还是关键字本身。
- B+树的叶子节点用链表串接,这样在执行范围查询的会更快。
⑤聚簇索引与非聚簇索引:通常我们会认为所有的索引树的叶子节点data域存放的都是行数据的指针,但是innodb对此做出了优化,针对主键和非主键发明了聚簇索引和非聚簇索引
聚簇索引:inndb的主键会生成聚簇索引,这种索引树的data域中存放的不再是行数据的指针,而直接就是行数据。
非聚簇索引:innodb的非主键索引结构就是聚簇索引,也叫辅助索引,这种索引树中的data域中存放的也不再是行数据的指针,而是主键的指针。然后再通过主键的索引树找到行数据。
3.覆盖索引和多值索引
- 覆盖索引与回表操作:如果查询的列,通过索引树的信息直接返回,则该索引称之为查询SQL的覆盖索引。
例如上面所示,表table建立了name和id索引。sql语句select id,name from table where name=b
就是覆盖索引,因为该sql先查询name索引树,然后通过name索引树查询到了主键值(接着它会查询id索引树,然后得到行信息)但是由于该sql只查询id,name字段,通过name索引树的信息就足够了,不需要再查询id索引树,这种sql就称作覆盖索引。在explain分析时就会Extra字段显示Using index,例如下图:explain select id from user where name="ccc";
回表操作指的是在查询完非聚簇索引(name索引)后,还需要查询聚簇索引的(id索引)的行为,所以覆盖索引是不需要回表操作的,这样可以减少io操作。提高效率。
哪些场景可以利用索引覆盖来优化SQL:
全表count查询优化:select count(id) from user where name like "aa%"
将单列索引升级为联合索引:select age from user where name="aa"
可以在(name,age)上建立索引。select id,name from user where name="aa" order by age limit 0,10
可以在(name,age)上建立索引
索引下推优化(索引下推):索引下推是利用联合索引优势,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。例如在(name,age)上建立联合索引,查询sqlSELECT * from user where name like '陈%' and age=20
如果不使用索引下推,则需要回表查询前两条记录。如果使用索引下推,它会先根据非聚簇索引中的值过滤掉不符合条件的,再回表查询。可以减少io次数。
4.建立索引后,使用sql的规范
①左前缀匹配原则,使用like查询"姓张或者名字带三的人"时,如果是"张%"则可以使用索引查询,如果是"%三"则不能使用索引查询。
②列与列比较不会使用索引,例如,字段name(姓名)和alias(外号)都建立了索引,但是select * from user where name=alias
则不能使用索引。
③索引字段不能使用计算或者函数,例如查询age(年龄)大于20的用户,如果使用select * from user where age/20>1
则不会使用索引,因为需要对每一个age进行计算,所以优化器无法预测age索引树的大概范围。如果使用select * from user where age>1*20
就会查询索引,因为优化器会先计算出20*1
的结果,然后再去索引查询。
④尽量不要使用select *
操作,因为这样可能使用不了覆盖索引(见上面覆盖索引)。当然如果执行select age from table where name=0
则不能使用覆盖索引了,因为name索引树中没有age的信息。不仅如此,像BOLB,TEXT等类型的字段返回,也会加重磁盘寻址和内存的的负担,降低查询效率。假设表table中只有id和name字段,如果执行
select * from table
则会查询name索引树。
这是因为id,name这些字段都可以在name索引树中得到,这比全表扫描有效的多。那为什么不查询id索引树呢?因为id索引树中的信息记录的是行数据,它不仅有id和name,还有一些隐藏字段。但是如果表中还有其它非索引字段,例如age等,那么执行上述sql就会全表扫描。一条sql引发的思考⑤如果在字段a,b,c上使用了联合索引。根据匹配原则,则以a,b,c顺序编写的条件sql可以使用索引,例如以下sql可以使用索引
select * from table where a=0
select * from table where a=0 and b=0
select * from table where a=0 and b=0 and c=0
因为以上sql会先判断a,在判断b,最后判断c,符合索引的构造顺序。此外select * from table where a=0 and c=0
也会使用索引,因为mysql优化器会截断条件,然后先根据a筛选掉大量数据,剩下的数据再根据c进行全扫,避免全表扫描。
此外如果联合索引的某个字段使用了范围查找,则之后的字段就不能使用索引了,例如select * from table where a=0 and b>0 and c=0
其中ab可以使用索引,c不能。select * from table where a like 'a%'` and b=0 and c=0
其中a可以使用索引,bc不能。
⑥尽量使用关联查询代替子查询,并且确保关联字段有索引。虽然子查询会带来写法上的方便,但是子查询的效率比较低。原因大多说是因为子查询会产生临时表,但是我用explain试了一下,extra并没有出现Using temporary
。但是关联查询也有不适用的场景,比如分表分库的情况下。
⑦可以拆分复杂查询为多个sql,这样每个拆分后的查询有可能利用索引,但是复杂查询极有可能利用不上索引。
⑧order by与group by 尽量使用到索引,否则mysql会使用filesort,降低查询效率,filesort是指无法利用索引时,使用临时表进行排序,分为单路排序和双路排序双路排序: 1.将符合条件的记录的排序字段 (例如 createTime) 和主键 id 这两个字段放到 sort buffer(排序缓存) 中 2.对 sort_buffer 中数据按照字段 createTime 进行排序 3.遍历排序好的 id 和字段 createTime,按照 id 的值回到原表中取出所有字段的值返回给客户端 单路排序: 1.将符合条件的记录的 所有字段放到 sort buffer(排序缓存) 中 2.对 sort_buffer 中数据按照字段 createTime 进行排序 3.遍历排序好的记录数据直接返回给客户端
note:单路排序相比于双路排序,减少了磁盘的寻址过程,加快了查询时间,但是同时也增加了内存消耗。
note:同时需要注意where查询条件需要与order by/group by排序条件使用同一个索引,并且和where查询字段一样遵循索引规则(例如按照索引顺序等等),否则也将使用filesort。以下sql就是一些反例:1)对索引列同时使用了ASC和DESC explain select id from user order by age asc,name desc; //对应(age,name)索引 2)WHERE子句和ORDER BY子句满足最左前缀,但where子句使用了范围查询 explain select id from user where age>10 order by name; //对应(age,name)索引 3)ORDER BY或者WHERE+ORDER BY索引列没有满足联合索引建立规则 explain select id from user order by name; //对应(age,name)索引 4)WHERE子句与ORDER BY子句,MySQL每次只采用一个索引,使用了不同的索引 explain select id from user where name=‘aa’ order by age; //对应 (name)、(age)索引
⑨union可以用到将多个表的结果整合在一起,比如历史记录表和当前记录表这种情况。也可以代替or,in查询。mysql 实战 or、in与union all 的查询效率
1)当查询条件为索引时可以使用union 或者union all 代替or查询,or查询会使用全表扫描,导致查询比较慢;
2)但是当查询条件非索引时,就不需要使用了union了,因为非索引字段本来就需要扫全表,使用union只会增加扫表的次数。而且,使用union也要注意场景,例如排序和分页就不太擅长
3)尽可能使用union all代替union。因为union需要去重,无形中增加排比较的负担。note:使用in或者not in查询时,可能会使用索引,因为mysql优化器会根据in条件预测,如果索引效率高就会使用索引,否则走全表扫描,至于怎样的过滤算是效率高,就要看mysql优化器的分析了。
索引建立的规范 回表与覆盖索引,索引下推
①尽量按排序顺序建立索引:排序会占用大量的cpu和内存,索引尽量按照order by的顺序建立索引。
②长字段尽量使用前缀来做索引,如果需要在长字段上建索引,可以使用一部分前缀建立索引值。因为过长的字段建立索引会导致聚簇索引树和辅助索引的data域比较大,而且查找过程中的比较也会浪费时间。
③尽量使用联合索引,如果有多个and条件,mysql优化器只会查询过滤条件高的索引列,所以少建单列索引。
④减少重复、冗余以及未使用的索引,如果已经创建了索引(A,B),那么再创建索引(A)的话,就属于重复索引。因为单独查询A列时就可以使用索引(A,B),但是单独B列用不到索引。
⑤索引值不要随意更新(包括删除),因为这样会到B+tree频繁分裂保持平衡。这也是我们不删除记录,而使用is_del字段标记的其中一个原因。
5.sql分析
1)explain:用来分析sql语句实际执行情况的语法,因为我们编写的sql与mysql优化器实际执行的逻辑并不一定相符。所以需要通过explain来验证sql语句的实际执行过程。
explain命令输出各种不同的格式,形如 explain format=traditional(默认值)|json|tree
explain会输出以下比较重要的字段信息
字段 | 说明 |
---|---|
id | 表示查询中执行 select 子句或操作表的顺序,id 值越大优先级越高,越先被执行。id 相同,执行顺序由上至下。 |
select_type | 每个select子句的类型 |
type | 查询类型 |
table | 查询使用的表 |
possible_keys | mysql认为可能会使用的索引 |
keys | mysql执行过程中实际使用到的索引 |
key_len | 索引长度 |
ref | 显示索引的哪一列被使用了 |
rows | mysql估算需要查询的行数 |
filtered | 表示符合查询条件的数据与rows的比值 |
extra | 额外信息,含有很多执行过程中的重要信息,例如临时表,文件排序等 |
接下会逐个说明几个比较重要字段的意思
示例数据库包含两张表一张user表,一张home(其中含有user_id字段关联user主键),表字段分别为:
user(id, name, age, birthday) //(name, age)索引
home(id, name, user_id) //user_id索引
①id表示每个select子句的查询顺序,id越大越先执行,id相同时,从上往下依次执行
explain select * from home where user_id in(select id from user where name in ("aa", "bb"));
这张图片说明,上面的sql语句分为两步,第一步查询user表,第二步查询home
②select_type:表示查询语句每步的操作类型,包含以下几种
类型 | 说明 |
---|---|
SIMPLE | 简单的select 查询,SQL中不包含SUBQUERY或者UNION |
PRIMARY | 当查询语句中包含UNION或者SUBQUERY时,外层查询被标记为PRIMARY |
SUBQUERY | 在select 或者WHERE 列表中包含了复杂子查询 |
DERIVED | 在FROM列表中包含的复杂子查询会被标记为DERIVED(衍生表),复杂子查询会把结果集放到临时表中 |
UNION | 如果第二个SELECT 出现在UNION之后,则第二个SELECT 被标记位UNION;如果UNION包含在FROM子句的子查询中,则外层SELECT 将被标记为DERIVED |
UNION RESULT | 从UNION表获取结果的SELECT,UNION ALL 查询不会出现此类型 |
DEPENDENT SUBQUERY | 如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为相关子查询。当出现此值时说明sql需要优化 |
DEPENDENT UNION | 当内部union需要依赖外部查询时,内部union为此值,当出现此值时说明sql需要优化 |
note:上图中的in子查询可能会被mysql执行器优化成join连接,所以不一定出现SUBQUERY
explain select * from user where id =(select user_id from home where id = 1)
explain select * from home where user_id in(select id from user where age>10)
explain select * from home where user_id in(select id from user where age>(select avg(age) from user));
note:只有使用union时才会出现union result,因为union结果不重复,所以需要使用临时表存放数据进行比较。以下是union与union result两种差异
explain select id from user where id>1 union select id from user where age>10
explain select * from user where id>1 union all select * from user where age>10
explain select h.*, a.* from home h left join (select id from user where id>1 union select id from user where age>10) as a on a.id = h.user_id
note:目前还不清楚什么原因会出现相关子查询,但是如果in后接复杂子查询就有可能出现该值。该值与传统的子查询不同,传统的子查询是将内层查询的最终结果当作外层查询的条件,但是相关子查询会将外层查询结果依次交给内层查询计算,所以效率极差。
explain select * from home where user_id in (select id from user where id>1 union select id from user where age>10)
③table:查询的表
④type:查询性能,有以下常见的参数
类型 | 说明 |
---|---|
system | 表中只有一行数据或者是空表,且只能用于myisam和memory表。如果是Innodb引擎表,type列在这个情况通常都是all或者index |
const | 使用唯一索引或者主键,返回1行记录的等值常量查询时,例如:select * from user where id=1 |
eq_ref | 当两表关联查询并且都使用唯一索引建立连接时,属于该值。例如:select * from user u left join home h on u.id = h.id |
ref | 非唯一索引的等值查询,或者两表关联查询但不全是使用的唯一索引建立连接时,例如:select * from user u left join home h on u.id = h.user_id |
range | WHERE 语句中出现between 、< 、> 、in 等查询条件时,此时只需要与索引做比较即可 |
index | 遍历索引树,无需查表即可得到需要的数据。常出现在覆盖索引的语句中,例如,select id, name from user |
all | 扫描全表,当上述条件都不成立时查询表并依次比较, 此时性能最差,需要优化sql |
note:上面的查询性能从上到下依次降低,编写sql语句时尽量减少all类型。
⑤keys:实际使用的索引,mysql实际执行过程中只会使用一个索引,此值可以看到实际使用到了哪个索引。
⑥rows:每个执行过程需要查询的记录行数。
⑦extra:执行过程中的额外信息,虽然说是额外信息,但是反应了执行过程中的一些重要信息,有以下常用的参数。
类型 | 说明 |
---|---|
Using where | 使用where条件 |
Using index | 表示执行了覆盖索引,不需要回表即可查询到所需数据;例如:select id from user where name in ("aa", "bb") |
Using index condition | 查询条件使用了非主键索引,例如,in,like, > ,<等范围查询 |
Using join buffer (hash join) | 如果两表关联,但是关联字段没有建立索引就会出现该值。表示两表关联时无法利用索引树快速完成连接,hash join表示未使用索引的情况下使用的连接算法。出现此值说明需要关联字段建立索引 |
Using temporary | 使用临时表,常用排序,分组,去重(distinct)等语句,可以进行优化 |
Using filesort | 使用文件排序,mysql无法按照既有的索引顺序进行完成排序, 排序时尽量按照索引规则排序 |
2)慢查询分析
如何中程序中庞大的sql中查找出执行效率不高或者没有使用索引的sql?mysql可以开启慢查询日志
①show variables like 'slow_query_log';
查询是否开启慢查询日志
set global slow_query_log = 1/on(0/off);
开启或关闭慢查询
②show variables like 'log_queries_not_using_indexes'
查询未使用索引的日志
set global log_queries_not_using_indexes=1/on(0/off)
开启或者关闭未使用索引的日志
③show variables like 'long_query_time';
查询慢查询日志的阈值
set global long_query_time= 10
设置慢查询日志的阈值
④show variables like 'slow_query_log_file%';
查询慢查询日志的生成路径
3)mysqldumpslow:它是mysql提供的一个慢查询日志文件分析器,可以添加具体的参数从慢查询日志文件中查询到符合指定条件的sql语句,参数如下:
参数 | 说明 |
---|---|
-s | 表示排序规则,包括c、t、l、r分别是按照查询次数、查询锁定时间、查询时间、返回的记录数 |
-t | 查询指定条数,形如 -t 10 |
-g | 查询时使用的正则判断 |
示例命令:
1 访问次数最多的20个sql语句
mysqldumpslow -s c -t 20 slow.log
2 返回记录集最多的20个sql
mysqldumpslow -s r -t 20 slow.log
3 得到按照时间排序的前10条里面含有做了连接的查询SQL
mysqldumpslow -s t -t 10 -g "left join" slow.log
注意点:当两表关联并存在范围查询和分页时,需要注意limit0,20与limit 10,20的执行逻辑不同,limit 0,20时会先根据各表的范围查询过滤,在进行hash连表。所以存在范围查询等级的问题,like查询最相似的会排在前面,进行连表时like相似的也会排在前面。但是limit 10,20时,如果关联字段没有建索引,mysql尝试使用最优的逻辑,但是两表可能都会失去索引功能,这时就不存在范围相似优先级的问题,索引前后两页查询时就有可能出现重复数据
参照:BTree和B+Tree详解
深入理解MySQL索引之B+Tree
MySQL 深入理解索引B+树存储 (二)
如何写一手好sql
mysql in语句与索引
MySQL Explain详解
sql中exists,not exists的用法
SQL中EXISTS的用法
sql查询每个用户最近一次登录记录
InnoDB的插入缓冲 - 知乎 (zhihu.com)
MySQL索引、架构(redolog, undolog, binlog)
主键索引和辅助索引的区别(MyISAM和InnoDB)
MySQL 慢查询