引言
架构调优,在系统设计时首先需要充分考虑业务的实际情况,是否可以把不适合数据库做的事情放到数据仓库、搜索引擎或者缓存中去做;然后考虑写的并发量有多大,是否需要采用分布式;最后考虑读的压力是否很大,是否需要读写分离。对于核心应用或者金融类的应用,需要额外考虑数据安全因素,数据是否不允许丢失。所以在进行优化时,首先需要关注和优化的应该是架构,如果架构不合理,即使是DBA能做的事情其实是也是比较有限的。
MySQL调优,需要确认业务表结构设计是否合理,SQL语句优化是否足够,该添加的索引是否都添加了,是否可以剔除多余的索引等等
本文已如上角度进行分析,当然,关于业务表结构是否合理的话题,在今后深入到MySQL底层再讨论。
同时,本文将介绍一些常用的提高索引效率的方案。
慢查询基础-数据访问
查询花费大量时间,超过long_query_time参数设定的时间阈值的SQL语句。
一般查询性能低下的最基本原因是访问的数据太多。
故,我们对于低效的查询一般通过两种步骤分析:是否检索了大量不需要的数据、是否超过需要的数据行。
检索了大量不需要的数据-列
总是取出全部列?
每次看到SELECT*的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、内存和CPU的消耗。因此,一些DBA是严格禁止SELECT *的写法的,这样做有时候还能避免某些列被修改带来的问题。
尤其是使用二级索引,使用*的方式会导致回表,导致性能低下。
什么时候可以使用?SELECT*如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。
重复查询相同的数据-加缓存
不断地重复执行相同的查询,然后每次都返回完全相同的数据。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。
衡量查询开销的指标
响应时间(服务时间+排队时间)、扫描行数、返回行数、访问类型。
如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化它:
1、使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果了
2、改变库表结构。例如使用单独的汇总表。
3、重写这个复杂的查询,让MySQL优化器能够以更优的方式执行该查询。
慢查询配置
set global long_query_time=10; (10秒)
l slow_query_log 启动停止慢查询日志
l slow_query_log_file 指定慢查询日志得存储路径及文件(默认和数据文件放一起)
l long_query_time 指定记录慢查询日志SQL执行时间得伐值(单位:秒,默认10秒)
l log_queries_not_using_indexes 是否记录未使用索引的SQL
l log_output 日志存放的地方可以是TABLE[FILE,TABLE]
Explian执行计划
EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划,我们需要搞懂EPLATNEXPLAIN的各个输出项都是干嘛使的,从而可以有针对性的提升我们查询语句的性能。
分析查询语句或是表结构的性能瓶颈,总的来说通过EXPLAIN我们可以:
l 表的读取顺序
l 数据读取操作的操作类型
l 哪些索引可以使用
l 哪些索引被实际使用
l 表之间的引用
l 每张表有多少行被优化器查询
type
我们前边说过执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法/访问类型,其中的type列就表明了这个访问方法/访问类型是个什么东西,是较为重要的一个指标,结果值从最好到最坏依次是:
出现比较多的是system>const>eq_ref>ref>range>index>ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。
查询优化器
SQL语句在MySQL中执行过程如图所示。
优化:优化SQL语句,例如重写查询,决定表的读取顺序,以及选择需要的索引等。这一阶段用户是可以查询的,查询服务器优化器是如何进行优化的,便于用户重构查询和修改相关配置,达到最优化。这一阶段还涉及到存储引擎,优化器会询问存储引擎,比如某个操作的开销信息、是否对特定索引有查询优化等。
高性能索引使用策略
不在索引列进行任何操作
尽量全值匹配
最佳左前缀法则
范围条件放在最后
覆盖索引尽量用
不等于慎用
Null/Not对查询有影响
小心Like查询
字符类型加上引号
小心使用or关键字
使用索引扫描来排序或分组
排序注意点
尽可能按照主键顺序插入行
优化Count查询
优化limit分页
NULL特别说明
不在索引列进行任何操作:
在索引列上使用函数,是无法利用索引的;索引列不进行计算操作,MySQL无法自动解析方程式。
尽量全值匹配:
建立了联合索引列后,如果我们的搜索条件中的列和索引列一致的话,这种情况就称为全值匹配。查询优化器会决定先用哪个后用哪个查询的条件~
最佳左前缀法则:
建立了联合索引列,如果搜索条件不够全值匹配怎么办?在我们的搜索语句中也可以不用包含全部联合索引中的列,但要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。PS:建立联合索引只会产生一颗B+树
范围条件放最后
所有记录都是按照索引列的值从小到大的顺序排好序的,而联合索引则是按创建索引时的顺序进行分组排序。如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引。对于一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找;而中间有范围查询会导致后面的列全部失效,无法充分利用这个联合索引
覆盖索引尽量用(不回表)
覆盖索引是非常有用的工具,能够极大地提高性能,三星索引里最重要的那颗星就是宽索引星。如果查询只需要扫描索引而无须回表,
索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中。
因为索引是按照列值顺序存储的,所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多。
由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。
尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),不是必要的情况下减少select*,除非是需要将表中的全部列检索后,进行缓存。
慎用不等于(mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描)
小心Like查询
like以通配符开头('%abc...'),mysql索引失效会变成全表扫描的操作。解决方案是使用覆盖索引涉及到的列进行like查询。
字符类型加引号,不加单引号导致索引失效奥
原因:MySQL的查询优化器,会自动的进行类型转换,自然造成索引失效。
使用or关键字时要注意
使用同一列进行or,没啥。如果or的两列不是同一列,某个不是索引,就只能全盘扫描。这种情况下可以通过union all 或者 覆盖扫描 改善这种问题。
使用索引扫描来排序和分组
MySQL有两种方式可以生成有序的结果﹔通过排序操作﹔或者按索引顺序扫描施﹔如果EXPLAIN出来的type列的值为“index”,则说明MySQL使用了索引扫描来做排序。
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在IO密集型的工作负载时。
MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。
排序小心
排序列包含非同一个索引的列的情况,不能用索引进行排序。
尽可能按主键顺序插入行
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用。例如,从性能的角度考虑,使用UUID来作为聚簇索引则会很糟糕,它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。
最简单的方法是使用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。
注意到向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大。这一方面是由于主键字段更长﹔另一方面毫无疑问是由于页分裂和碎片导致的。
因为主键的值是顺序的,所以InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出部分空间用于以后修改),下一条记录就会写入新的页中。一旦数据按照这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果。
如果新行的主键值不一定比之前插入的大,所以InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置-—通常是已有数据的中间位置——并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。下面是总结的一些缺点:
写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机IO。
因为写入是乱序的,InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
所以使用InnoDB时应该尽可能地按主键顺序插入数据,并且尽可能地使用单调增加的聚簇键的值来插入新行。
优化count查询
COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。
通常来说,COUNT()都需要扫描大量的行(意味着要访问大量数据)才能获得精确的结果,因此是很难优化的。在MySQL层面能做的基本只有索引覆盖扫描了。如果这还不够,就需要考虑修改应用的架构,可以用估算值取代精确值,可以增加汇总表,或者增加类似Redis这样的外部缓存系统。
优化limit分页查询
先查询翻页中需要的N条数据的主键值,然后根据主键值回表查询所需要的N条数据,在此过程中查询N条数据的主键id在索引中完成,所以效率会高一些。
EXPLAIN SELECT * FROM (select id from order_exp limit 10000,10) b,order_exp
a where a.id = b.id;
select * from order_exp limit 10000,10; 这种偏移量大,相当于舍弃1w条数据,只要后面10条,这也是查询远超出需要的数据列。
关于NULL的特别说明
NULL 在MySQL中是独一无二的!MySQL8 在进行索引列的数据统计行为把null视为nulls_equal情况(NULL值在业务上就是代表没有,所有的NULL值和起来算一份),看起来,MySQL中对Null值的处理也很分裂。所以总的来说,对于列的声明尽可能的不要允许为null。