接上:Mysql调优(二)——schema与数据类型优化、执行计划
本篇文章在一定程度上与下面两篇文章重合,笔者建议优先读下面两篇文章,本文偏向概念,而它们偏向原理:
Mysql索引的本质深入浅出(一)
Mysql索引的本质深入浅出(二)
一、通过索引进行优化
想要了解索引的优化方式,必须要对索引的底层原理有所了解
1.1 索引基本知识
1.1.1 索引的优点
- 大大减少了服务器需要扫描的数据量;
- 帮助服务器避免排序和临时表;
- 将随机io变成顺序io;
1.12 索引的用处
- 快速查找匹配WHERE子句的行;
- 从consideration中消除行,如果可以在多个索引之间进行选择,mysql通常会使用找到最少行的索引;
- 如果表具有多列索引,则优化器可以使用索引的任何最左前缀来查找行;
- 当有表连接的时候,从其他表检索行数据;
- 查找特定索引列的min或max值;
- 如果排序或分组时在可用索引的最左前缀上完成的,则对表进行排序和分组;
- 在某些情况下,可以优化查询以检索值而无需查询数据行
1.1.3 索引的分类
- 主键索引;
- 唯一索引;
- 普通索引;
- 全文索引;
- 组合索引
1.1.4 面试技术名词
- 回表;
- 覆盖索引;
- 最左匹配;
- 索引下推;
1.1.5 索引采用的数据结构**
- 哈希表;
- B+树
另外还有几个概念:
- 页分裂;
- 页合并;
- 索引合并;
1.1.6 索引匹配方式
create table staffs(
id int primary key auto_increment,
name varchar(24) not null default '' comment '姓名',
age int not null default 0 comment '年龄',
pos varchar(20) not null default '' comment '职位',
add_time timestamp not null default current_timestamp comment '入职时间'
) charset utf8 comment '员工记录表';
-----------索引
alter table staffs add index idx_nap(name, age, pos);
-
全值匹配
-
全值匹配指的是和索引中的所有列进行匹配
- explain select * from staffs where name = 'July' and age = '23' and pos = 'dev';
-
-
匹配最左前缀
- 只匹配前面的几列
explain select * from staffs where name = 'July' and age = '23'; explain select * from staffs where name = 'July';
-
匹配列前缀
- 可以匹配某一列的值的开头部分
explain select * from staffs where name like 'J%'; explain select * from staffs where name like '%y';
-
匹配范围值
-
可以查找某一个范围的数据
- explain select * from staffs where name > ‘Mary’;
-
-
精确匹配某一列并范围匹配另外一列
- 可以查询第一列的全部和第二列的部分
explain select * from staffs where name = 'July' and age > 25;
-
只访问索引的查询
- 查询的时候只需要访问索引,不需要访问数据行,本质上就是覆盖索引
explain select name,age,pos from staffs where name = 'July' and age = 25 and pos = 'dev';
- 查询的时候只需要访问索引,不需要访问数据行,本质上就是覆盖索引
1.2 哈希索引
-
基于哈希表的实现,只有精确匹配索引所有列的查询才有效;
-
在mysql中,只有memory的存储引擎显式支持哈希索引
-
哈希索引自身只需存储对应的hash值,所以索引的结构十分紧凑,这让哈希索引查找的速度非常快
-
哈希索引的限制
- 1、哈希索引只包含哈希值和行指针,而不存储字段值,索引不能使用索引中的值来避免读取行
- 2、哈希索引数据并不是按照索引值顺序存储的,所以无法进行排序
- 3、哈希索引不支持部分列匹配查找,哈希索引是使用索引列的全部内容来计算哈希值
- 4、哈希索引支持等值比较查询,也不支持任何范围查询
- 5、访问哈希索引的数据非常快,除非有很多哈希冲突,当出现哈希冲突的时候,存储引擎必须遍历链表中的所有行指针,逐行进行比较,直到找到所有符合条件的行
- 6、哈希冲突比较多的话,维护的代价也会很高
-
案例
当需要存储大量的URL,并且根据URL进行搜索查找,如果使用B+树,存储的内容就会很大:select id from url where url=""
也可以利用将url使用CRC32做哈希,可以使用以下查询方式:
select id fom url where url="" and url_crc=CRC32("")
此查询性能较高原因是使用体积很小的索引来完成查找
1.3 组合索引
当包含多个列作为索引,需要注意的是正确的顺序依赖于该索引的查询,同时需要考虑如何更好的满足排序和分组的需要;
1.4 聚簇索引与非聚簇索引
1.4.1 聚簇索引
- 不是单独的索引类型,而是一种数据存储方式,指的是数据行跟相邻的键值紧凑的存储在一起
-
优点
- 1、可以把相关数据保存在一起
- 2、数据访问更快,因为索引和数据保存在同一个树中
- 3、使用覆盖索引扫描的查询可以直接使用页节点中的主键值
-
缺点
- 1、聚簇数据最大限度地提高了IO密集型应用的性能,如果数据全部在内存,那么聚簇索引就没有什么优势
- 2、插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式
- 3、更新聚簇索引列的代价很高,因为会强制将每个被更新的行移动到新的位置
- 4、基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂的问题
- 5、聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候
-
1.4.2 非聚簇索引
- 数据文件跟索引文件分开存放
1.5 覆盖索引
-
基本介绍
- 1、如果一个索引包含所有需要查询的字段的值,我们称之为覆盖索引
- 2、不是所有类型的索引都可以称为覆盖索引,覆盖索引必须要存储索引列的值
- 3、不同的存储实现覆盖索引的方式不同,不是所有的引擎都支持覆盖索引,memory不支持覆盖索引
-
优势
- 1、索引条目通常远小于数据行大小,如果只需要读取索引,那么mysql就会极大的较少数据访问量
- 2、因为索引是按照列值顺序存储的,所以对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO要少的多
- 3、一些存储引擎如MYISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用,这可能会导致严重的性能问题
- 4、由于INNODB的聚簇索引,覆盖索引对INNODB表特别有用
-
案例演示
- 覆盖索引.md
1.6 优化小细节
-
当使用索引列进行查询的时候尽量不要使用表达式,把计算放到业务层而不是数据库层
select actor_id from actor where actor_id=4;
select actor_id from actor where actor_id+1=5;
-
尽量使用主键查询,而不是其他索引,因为主键查询不会触发回表查询
-
使用前缀索引
- 前缀索引实例说明.md
-
使用索引扫描来排序
- 使用索引扫描来做排序.md
-
union all,in,or都能够使用索引,但是推荐使用in
explain select * from actor where actor_id = 1 union all select * from actor where actor_id = 2; explain select * from actor where actor_id in (1,2); explain select * from actor where actor_id = 1 or actor_id =2;
-
范围列可以用到索引
- 范围条件是:<、>
- 范围列可以用到索引,但是范围列后面的列无法用到索引,索引最多用于一个范围列
-
强制类型转换会全表扫描
不会触发索引:create table user(id int,name varchar(10),phone varchar(11)); alter table user add index idx_1(phone); explain select * from user where phone=13800001234;
触发索引:
explain select * from user where phone='13800001234';
-
更新十分频繁,数据区分度不高的字段上不宜建立索引
- 更新会变更B+树,更新频繁的字段建议索引会大大降低数据库性能
- 类似于性别这类区分不大的属性,建立索引是没有意义的,不能有效的过滤数据,
- 一般区分度在80%以上的时候就可以建立索引,区分度可以使用 count(distinct(列名))/count(*) 来计算
-
创建索引的列,不允许为null,可能会得到不符合预期的结果
-
当需要进行表连接的时候,最好不要超过三张表,因为需要join的字段,数据类型必须一致
-
能使用limit的时候尽量使用limit
-
单表索引建议控制在5个以内
-
单索引字段数不允许超过5个(组合索引)
-
创建索引的时候应该避免以下错误概念
- 索引越多越好
- 过早优化,在不了解系统的情况下进行优化
1.7 索引监控
show status like 'Handler_read%';
参数解释:
- Handler_read_first:读取索引第一个条目的次数
- Handler_read_key:通过index获取数据的次数
- Handler_read_last:读取索引最后一个条目的次数
- Handler_read_next:通过索引读取下一条数据的次数
- Handler_read_prev:通过索引读取上一条数据的次数
- Handler_read_rnd:从固定位置读取数据的次数
- Handler_read_rnd_next:从数据节点读取下一条数据的次数
1.8 简单案例
- 索引优化分析案例.md
二、查询优化
在编写快速的查询之前,需要清楚一点,真正重要的是响应时间,而且要知道在整个SQL语句的执行过程中每个步骤都花费了多长时间,要知道哪些步骤是拖垮执行效率的关键步骤,想要做到这点,必须要知道查询的生命周期,然后进行优化,不同的应用场景有不同的优化方式,不要一概而论,具体情况具体分析,
2.1 查询慢的原因
- 网络
- CPU
- IO
- 上下文切换
- 系统调用
- 生成统计信息
- 锁等待时间
2.2 优化数据访问
-
查询性能低下的主要原因是访问的数据太多,某些查询不可避免的需要筛选大量的数据,我们可以通过减少访问数据量的方式进行优化
- 确认应用程序是否在检索大量超过需要的数据
- 确认mysql服务器层是否在分析大量超过需要的数据行
-
是否向数据库请求了不需要的数据
-
查询不需要的记录
我们常常会误以为mysql会只返回需要的数据,实际上mysql却是先返回全部结果再进行计算,在日常的开发习惯中,经常是先用select语句查询大量的结果,然后获取前面的N行后关闭结果集。
优化方式是在查询后面添加limit
-
多表关联时返回全部列
select * from actor inner join film_actor using(actor_id) inner join film using(film_id) where film.title=‘Academy Dinosaur’;
select actor.* from actor…;
-
总是取出全部列
在公司的企业需求中,禁止使用select *,虽然这种方式能够简化开发,但是会影响查询的性能,所以尽量不要使用
-
重复查询相同的数据
如果需要不断的重复执行相同的查询,且每次返回完全相同的数据,因此,基于这样的应用场景,我们可以将这部分数据缓存起来,这样的话能够提高查询效率
-
2.3 执行过程的优化
2.3.1 查询缓存
在解析一个查询语句之前,如果查询缓存是打开的,那么mysql会优先检查这个查询是否命中查询缓存中的数据,如果查询恰好命中了查询缓存,那么会在返回结果之前会检查用户权限,如果权限没有问题,那么mysql会跳过所有的阶段,就直接从缓存中拿到结果并返回给客户端。
2.3.2 查询优化处理
mysql查询完缓存之后会经过以下几个步骤:解析SQL、预处理、优化SQL执行计划,这几个步骤出现任何的错误,都可能会终止查询
2.3.2.1 语法解析器和预处理
mysql通过关键字将SQL语句进行解析,并生成一颗解析树,mysql解析器将使用mysql语法规则验证和解析查询,例如验证使用使用了错误的关键字或者顺序是否正确等等,预处理器会进一步检查解析树是否合法,例如表名和列名是否存在,是否有歧义,还会验证权限等等。
2.3.2.2 查询优化器
当语法树没有问题之后,相应的要由优化器将其转成执行计划,一条查询语句可以使用非常多的执行方式,最后都可以得到对应的结果,但是不同的执行方式带来的效率是不同的,优化器的最主要目的就是要选择最有效的执行计划。
mysql使用的是基于成本的优化器,在优化的时候会尝试预测一个查询使用某种查询计划时候的成本,并选择其中成本最小的一个
-
数据经过一系列的统计信息计算而来
select count(*) from film_actor; show status like 'last_query_cost';
可以看到这条查询语句大概需要做1104个数据页才能找到对应的数据,这是经过一系列的统计信息计算来的:
- 每个表或者索引的页面个数
- 索引的基数
- 索引和数据行的长度
- 索引的分布情况
-
在很多情况下mysql会选择错误的执行计划,原因如下:
- 统计信息不准确:
InnoDB因为其mvcc的架构,并不能维护一个数据表的行数的精确统计信息
- 执行计划的成本估算不等同于实际执行的成本:
有时候某个执行计划虽然需要读取更多的页面,但是他的成本却更小,因为如果这些页面都是顺序读或者这些页面都已经在内存中的话,那么它的访问成本将很小,mysql层面并不知道哪些页面在内存中,哪些在磁盘,所以查询之际执行过程中到底需要多少次IO是无法得知的
- mysql的最优可能跟你想的不一样:
mysql的优化是基于成本模型的优化,但是有可能不是最快的优化
- mysql不考虑其他并发执行的查询
- mysql不会考虑不受其控制的操作成本:
执行存储过程或者用户自定义函数的成本
-
优化器的优化策略
-
静态优化
- 直接对解析树进行分析,并完成优化
-
动态优化
- 动态优化与查询的上下文有关,也可能跟取值、索引对应的行数有关
-
mysql对查询的静态优化只需要一次,但对动态优化在每次执行时都需要重新评估
-
-
优化器的优化类型
- 重新定义关联表的顺序:
数据表的关联并不总是按照在查询中指定的顺序进行,决定关联顺序时优化器很重要的功能
- 将外连接转化成内连接,内连接的效率要高于外连接
- 使用等价变换规则,mysql可以使用一些等价变化来简化并规划表达式
- 优化count(),min(),max():
索引和列是否可以为空通常可以帮助mysql优化这类表达式:例如,要找到某一列的最小值,只需要查询索引的最左端的记录即可,不需要全文扫描比较
- 预估并转化为常数表达式,当mysql检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行处理:
explain select film.film_id,film_actor.actor_id from film inner join film_actor using(film_id) where film.film_id = 1
- 索引覆盖扫描,当索引中的列包含所有查询中需要使用的列的时候,可以使用覆盖索引
- 子查询优化:
mysql在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问,例如将经常查询的数据放入到缓存中
- 等值传播
如果两个列的值通过等式关联,那么mysql能够把其中一个列的where条件传递到另一个上:
explain select film.film_id from film inner join film_actor using(film_id ) where film.film_id > 500;
这里使用film_id字段进行等值关联,film_id这个列不仅适用于film表而且适用于film_actor表
explain select film.film_id from film inner join film_actor using(film_id ) where film.film_id > 500 and film_actor.film_id > 500;
3 -
关联查询
mysql的关联查询很重要,但其实关联查询执行的策略比较简单:mysql对任何关联都执行嵌套循环关联操作,即mysql先在一张表中循环取出单条数据,然后再嵌套到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。mysql会尝试再最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行之后,mysql返回到上一层次关联表,看是否能够找到更多的匹配记录,以此类推迭代执行。整体的思路如此,但是要注意实际的执行过程中有多个变种形式:-
join的实现方式原理
- Simple Nested-Loop Join
- Index Nested-Loop Join
- Block Nested-Loop Join:
(1)Join Buffer会缓存所有参与查询的列而不是只有Join的列;
(2)可以通过调整join_buffer_size缓存大小;
(3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL5.1.22版本前是4G-1,而之后的版本才能在64位操作系统下申请大于4G的Join Buffer空间;
(4)使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loop为on,默认为开启。show variables like ‘%optimizer_switch%’
- 案例演示
(1)查看不同的顺序执行方式对查询性能的影响:
explain select film.film_id,film.title,film.release_year,actor.actor_id,actor.first_name,actor.last_name from film inner join f ilm_actor using(film_id) inner join actor using(actor_id);
(2)查看执行的成本: show status like ‘last_query_cost’;
(3)按照自己预想的规定顺序执行:
explain select straight_join film.film_id,film.title,film.release_year,actor.actor_id,actor.first_name,actor.last_name from film inner join film_actor using(film_id) inner join actor using(actor_id);
(4)查看执行的成本:
show status like ‘last_query_cost’; -
-
排序优化
无论如何排序都是一个成本很高的操作,所以从性能的角度出发,应该尽可能避免排序或者尽可能避免对大量数据进行排序。
推荐使用利用索引进行排序,但是当不能使用索引的时候,mysql就需要自己进行排序,如果数据量小则再内存中进行,如果数据量大就需要使用磁盘,mysql中称之为filesort。
如果需要排序的数据量小于排序缓冲区(show variables like ‘%sort_buffer_size%’😉,mysql使用内存进行快速排序操作,如果内存不够排序,那么mysql就会先将树分块,对每个独立的块使用快速排序进行排序,并将各个块的排序结果存放再磁盘上,然后将各个排好序的块进行合并,最后返回排序结果-
排序的算法
- 两次传输排序
第一次数据读取是将需要排序的字段读取出来,然后进行排序,第二次是将排好序的结果按照需要去读取数据行。
这种方式效率比较低,原因是第二次读取数据的时候因为已经排好序,需要去读取所有记录而此时更多的是随机IO,读取数据成本会比较高。
两次传输的优势,在排序的时候存储尽可能少的数据,让排序缓冲区可以尽可能多的容纳行数来进行排序操作- 单次传输排序
先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果,此方式只需要一次顺序IO读取所有的数据,而无须任何的随机IO,问题在于查询的列特别多的时候,会占用大量的存储空间,无法存储大量的数据
- 当需要排序的列的总大小超过max_length_for_sort_data定义的字节,mysql会选择双次排序,反之使用单次排序,当然,用户可以设置此参数的值来选择排序的方式
-
2.4 优化特定类型的查询
.4.1 优化count()查询
count()是特殊的函数,有两种不同的作用,一种是某个列值的数量,也可以统计行数
-
总有人认为myisam的count函数比较快,这是有前提条件的,只有没有任何where条件的count(*)才是比较快的
-
使用近似值
在某些应用场景中,不需要完全精确的值,可以参考使用近似值来代替,比如可以使用explain来获取近似的值;
其实在很多OLAP的应用中,需要计算某一个列值的基数,有一个计算近似值的算法叫hyperloglog。 -
更复杂的优化
一般情况下,count()需要扫描大量的行才能获取精确的数据,其实很难优化,在实际操作的时候可以考虑使用索引覆盖扫描,或者增加汇总表,或者增加外部缓存系统。
2.4.2 优化关联查询
-
确保on或者using子句中的列上有索引,在创建索引的时候就要考虑到关联的顺序
当表A和表B使用列C关联的时候,如果优化器的关联顺序是B、A,那么就不需要再B表的对应列上建上索引,没有用到的索引只会带来额外的负担,一般情况下来说,只需要在关联顺序中的第二个表的相应列上创建索引。
-
确保任何的groupby和order by中的表达式只涉及到一个表中的列,这样mysql才有可能使用索引来优化这个过程
2.4.3 优化子查询
子查询的优化最重要的优化建议是尽可能使用关联查询代替
2.4.4优化limit分页
在很多应用场景中我们需要将数据进行分页,一般会使用limit加上偏移量的方法实现,同时加上合适的orderby 的子句,如果这种方式有索引的帮助,效率通常不错,否则的化需要进行大量的文件排序操作,还有一种情况,当偏移量非常大的时候,前面的大部分数据都会被抛弃,这样的代价太高。
要优化这种查询的话,要么是在页面中限制分页的数量,要么优化大偏移量的性能
- 优化此类查询的最简单的办法就是尽可能地使用覆盖索引,而不是查询所有的列:
select film_id,description from film order by title limit 50,5; explain select film.film_id,film.description from film inner join (select film_id from film order by title limit 50,5) as lim using(film_id);
2.4.5 优化union查询
mysql总是通过创建并填充临时表的方式来执行union查询,因此很多优化策略在union查询中都没法很好的使用。经常需要手工的将where、limit、order by等子句下推到各个子查询中,以便优化器可以充分利用这些条件进行优化
- 除非确实需要服务器消除重复的行,否则一定要使用union all,因此没有all关键字,mysql会在查询的时候给临时表加上distinct的关键字,这个操作的代价很高
2.4.6 推荐使用用户自定义变量
用户自定义变量是一个容易被遗忘的mysql特性,但是如果能够用好,在某些场景下可以写出非常高效的查询语句,在查询中混使用过程化和关系话逻辑的时候,自定义变量会非常有用。
用户自定义变量是一个用来存储内容的临时容器,在连接mysql的整个过程中都存在。
-
自定义变量的使用
- set @one :=1
- set @min_actor :=(select min(actor_id) from actor)
- set @last_week :=current_date-interval 1 week;
-
自定义变量的限制
- 1、无法使用查询缓存
- 2、不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名或者limit子句
- 3、用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信
- 4、不能显式地声明自定义变量地类型
- 5、mysql优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想地方式运行
- 6、赋值符号:=的优先级非常低,所以在使用赋值表达式的时候应该明确的使用括号
- 7、使用未定义变量不会产生任何语法错误
-
自定义变量的使用案例
-
优化排名语句
-
1、在给一个变量赋值的同时使用这个变量
select actor_id,@rownum:=@rownum+1 as rownum from actor limit 10;
-
2、查询获取演过最多电影的前10名演员,然后根据出演电影次数做一个排名
select actor_id,count(*) as cnt from film_actor group by actor_id order by cnt desc limit 10;
-
-
避免重新查询刚刚更新的数据:
- 当需要高效的更新一条记录的时间戳,同时希望查询当前记录中存放的时间戳是什么
update t1 set lastUpdated=now() where id =1; select lastUpdated from t1 where id =1;
update t1 set lastupdated = now() where id = 1 and @now:=now(); select @now;
-
确定取值的顺序
- 在赋值和读取变量的时候可能是在查询的不同阶段
set @rownum:=0; select actor_id,@rownum:=@rownum+1 as cnt from actor where @rownum<=1;
因为where和select在查询的不同阶段执行,所以看到查询到两条记录,这不符合预期。
set @rownum:=0; select actor_id,@rownum:=@rownum+1 as cnt from actor where @rownum<=1 order by first_name;
当引入了orde;r by之后,发现打印出了全部结果,这是因为order by引入了文件排序,而where条件是在文件排序操作之前取值的。
解决这个问题的关键在于让变量的赋值和取值发生在执行查询的同一阶段:
set @rownum:=0; select actor_id,@rownum as cnt from actor where (@rownum:=@rownum+1)<=1;
-