都有哪些维护可以进行数据库调优
关于数据库调优的知识点非常分散。不同的DBMS、不同的公司、不同的职位,不同的项目遇到的问题不尽相同。
虽然SQL查询优化的技术有很多,但是大方向上完成可以分成物理查询优化和逻辑查询优化两大块。
MySQL中提高性能的一个最有效的方式是对数据表设计合理的索引。索引提供了高效访问数据的方法,并且加快查询的速度,因此索引对查询的速度有着至关重要的影响。
大多数情况下都(默认)采用B+树来构建索引。只是空间列类型的索引使用R-树,并且MEMORY表还支持hash索引。
其实,用不用索引,最终都是优化器说了算。优化器是基于什么的优化器?基于cost开销(CostBaseOptimizer),它不是基于规则(Rule-BasedOptimizer),也不是基于语义。怎样开销小就怎么来。另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。
索引全局匹配的话,会极大的提高查询效率。
在MySQL建立联合索引时会遵守最佳左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
MySQL可以为多个字段创建索引,一个索引可以包括16个字段。对于多列索引,过滤条件要使用索引必须按照搜因建立的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。 如果查询条件中没有使用这些字段中第1个字段时,多列(或联合)索引不会被使用。
扩展:Alibaba《Java开发手册》:索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
对于一个使用InnoDB存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在聚簇索引的叶子节点的。而记录又是存储在数据页中的,数据页和记录又是按照记录主键值从小到大的顺序进行排序,所以如果我们插入的记录的主键值是依次增大的话,那我们每插满一个数据页就换到下一个数据也继续插,而如果我们插入的主键值忽大忽小的话,就比较麻烦了 。
假设某个数据页存储的记录已经满了,它存储的主键值在1~100之间,如果此时再插入一条主键值为9的记录,就需要把当前页面分裂成两个页面,把本页中的一些记录移动到新创建的页中。页面分裂和记录移动意味着什么?意味着:性能损耗! 所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的主键值依次递增,这样就不会发生这样的性能损耗了。
所以建议:让主键具有AUTO_INCREMENT,让存储引擎自己为表生成主键,而不是我们手动插入。
设计实体类属性时,一定要与数据库字段类型相对应,否则,就会出现类型转换的情况。
范围右边的列不能使用,比如:<、<=、>、>=和between等。
应用开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将查询条件放置WHERE语句最后。
结论:最好在设计数据表的时候就将字段设置为NOT NULL约束,比如你可以将INT类型的字段,默认值设置为0,将字符类型的默认值设置为空字符串。同理,在查询中使用not like 也无法使用索引,导致全表扫描。
在使用like关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引就不会起作用。只有“%”不在第一个位置,索引才会器作用。
Alibaba《Java开发手册》【强制】:页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
在WHERE子句中,如果在OR前的条件列进行了索引,而在OR后面的条件列没有进行索引吗,那么索引会失效。也就是说,OR前后的两个条件中的列都是索引时,查询中才使用索引。
因为OR的含义就是两个只要满足一个即可,因此只有一个条件列进行了索引时没有意义的,只要有条件列没有进行索引,就会进行全表扫描,因此索引的条件列也会失效。
统一使用utf8mb4(5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的字符进行比较需要进行转换会造成索引失效。
结论:对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的。如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表。在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表。“小表驱动大表”。
join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入的BNLJ算法来优化嵌套执行。
驱动表就是主表,被驱动表就是从表,非驱动表。
算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result…以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断。
可以看到这种方式效率时非常低的。当然MySQL肯定不会这么粗暴的去进行表的连接,所以就出现了后面的两种对Nested-Loop Join优化算法。
其优化的思路主要是为了==减少内层表数据的匹配次数,==所以要求被驱动表上必须有索引才行。通过外层表匹配条件直接与内存表索引进行匹配,避免与内存表的每条记录去进行比较,这样极大的减少了对内存表的匹配次数。
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表(外表)。
如果被驱动表加索引,效率时非常高的,但如果索引不是主键索引,还得进行一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。
如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录再加载到内存匹配,这样周而复始,大大增加了IO的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式。
不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。
注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的SQL中会分配N-1个Join Buffer。所以查询的时候尽量减少不必要的字段,可以让Join Buffer中可以存放更多的列。
select t1.b,t2.* from t1 straight_join t2 on (t1.b = t2.b) where t2.id <= 100; # 推荐
select t1.b,t2.* from t2 straight_join t1 on (t1.b = t2.b) where t2.id <= 100; # 不推荐
从MySQL的8.0.20版本开始将废弃BNLJ,开始加入了hash Join,默认都会使用Hash Join
Nested Loop:对于被链接的数据子集较小的情况,Nested Loop是个较好的选择。
Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用join key在内存中建立散列值,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。
MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。
子查询是MySQL的一项重要的功能,可以帮助我们通过一个SQL语句实现比较复杂的查询。但是子查询的执行效率不高。原因:
**在MySQL中,可以使用连接(JOIN)查询来替代子查询。**连接查询不需要建立临时表,其速度比子查询要快。如果查询中使用索引的话,性能就会更好。
尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL 替代。
在WHERE条件字段上加索引,但是为什么在ORDER BY字段上还要加索引呢?
在MySQL中支持两种排序方式,分别是FileSort和Index排序。
优化建议:
所有的排序都是在条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。
结论:
- 两个索引同时存在,MySQL自动选择最优的方案。但是,随着数据量的变化,选择的索引也会随之变化的。
- 当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然 。
filesort算法:双路排序和单路排序
排序的字段如果不在索引列上,则filesort会有两种算法:双路排序和单路排序
双路排序(慢):
- MySQL 4.1 之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据。读取行指针和order by 列,对它们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
- 从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。
取一批数据,再对磁盘进行两次扫描,众所周知,IO是很耗时的,所以在mysql4.1后,出现了第二种改进的算法,就是单路排序。
单路排序(快)
从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用跟多的空间,因为它把每一行都保存到内存中了。
结论及引申出的问题:
优化策略
SHOW VARIABLES LIKE '%sort_buffer_size%';
SHOW VARIABLES LIKE '%max_length_for_sort_data%' # 默认1024字节
Order by 时 select * 是一个大忌。最好只Query需要的字段。原因:
一般分页查询时,通过创建覆盖索引能够比较好地提高性能,一个常见又非常头疼的问题就是limit 2000000,10,此时需要MySQL排序前20000010记录,仅仅返回200000-2000010的记录,其他记录丢弃,查询排序的代价非常大。
EXPLAIN SELECT * FROM student LIMIT 2000000,10;
在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
EXPLAIN SELECT * FROM student t ,(SELECT id FROM student ORDER BY LIMIT 2000000,10)
a WHERE t.id = a.id;
该方案使用于主键自增的表,可以把limit查询转换成某个位置的查询
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
优点:
弊端:
索引字段的维护总是有代价的,因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者称为业务数据架构师的工作。
Index Condition Pushdown(ICP)是MySQL5.6中新特性,是一种在存储引擎层使用索引过滤数据的优化方式。
如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL服务器,由MySQL服务器评估WHERE后面的条件是否保留行。
启动ICP后,如果部分WHERE条件可以仅使用索引中的列进行筛选,则MySQL服务器会把这部分WHERE条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中读取行。
#关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=off';
#打开索引下推
SET optimizer_switch = 'index_condition_pushdown=on';
问题: 不太理解哪种情况下应该使用EXISTS,哪种情况应该用IN,选择的标准是看能否使用表的索引吗?
回答: 索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率时最高的。
问: 在MySQL中统计数据表的行数,可以使用三种方式:SELECT COUNT(*),SELECT COUNT(1)和 SELECT COUNT(具体字段),使用这三者之间的查询效率时怎样的?
答:前提,如果你要统计的某个字段是非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。
在表查询中,建议明确字段,不要使用*作为查询的字段列表,推荐使用SELECT<字段列表>查询。原因:
针对的是会扫描全表的SQL语句,如果你可以确定结果集只有一条,那么加上LIMIT 1的首,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT 1了。
只要有可能,在程序中尽量多使用COMMIT,这样程序的性能得到提高,需求也会因为COMMIT所释放的资源而减少。
COMMIT所释放的资源: