• MySQL查询优化与调优


    概述

    数据库调优的方式有多种:

    • 建立索引、充分利用到索引、不让索引失效
    • 对SQL语句进行优化
    • 调优如缓冲、线程数等参数
    • 数据过多时,分库分表

    大方向上可以分为物理查询优化逻辑查询优化两块。

    • 物理查询优化是通过索引和表连接方式等技巧来进行优化。
    • 逻辑查询优化是通过SQL等价变换提升查询效率。

    数据准备:

    1. CREATE TABLE `student_info` (
    2. `id` int NOT NULL AUTO_INCREMENT,
    3. `student_id` int NOT NULL,
    4. `name` varchar(20) DEFAULT NULL,
    5. `course_id` int NOT NULL,
    6. `class_id` int DEFAULT NULL,
    7. PRIMARY KEY (`id`)
    8. ) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8;
    1. CREATE TABLE `course` (
    2. `id` int NOT NULL AUTO_INCREMENT,
    3. `course_id` int NOT NULL,
    4. `course_name` varchar(40) DEFAULT NULL,
    5. PRIMARY KEY (`id`)
    6. ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;

    一、关联查询优化

    1.左(右)外连接

    • 没有索引的情况:
    1. mysql> explain select * from student_info s
    2. -> left join course c
    3. -> on s.course_id=c.course_id;
    4. +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
    5. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    6. +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
    7. | 1 | SIMPLE | c | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100.00 | NULL |
    8. | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 10.00 | Using where; Using join buffer (hash join) |
    9. +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+

    可以看到驱动表和被驱动表都没有索引(ALL),都是全表进行扫描。

    此时连接查询就相当于一个双层循环,驱动表的每一次循环都会去遍历被驱动表,消耗比较大。但查询优化器会进行优化,被驱动表的Extra字段是Using join buffer (hash join),后续会讲。

    执行计划中的第一条记录是驱动表,其余的都是被驱动表

    • 使用索引的情况:
    create index idx_ci on course(course_id); --给被驱动表的连接条件添加索引
    1. mysql> explain select * from student_info s
    2. -> left join course c
    3. -> on s.course_id=c.course_id;
    4. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
    5. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    6. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
    7. | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using where |
    8. | 1 | SIMPLE | c | NULL | ref | idx_ci | idx_ci | 4 | db1.s.course_id | 1 | 100.00 | NULL |
    9. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+

    此时被驱动表的type为ref,使用到了索引,加快了被驱动表的搜索。(此例中给驱动表加索引是没有意义的,因为要全表数据)

    2.内连接

    内连接和外连接没有索引时是相同的,都使用join buffer加快连接速度,但是内连接的特性是主表和从表地位是相同的,位置可以改变,所以优化器会根据情况优化修改两者的位置(所以我们也可以将外连接转内连接,这样sql语句可以享受更多的优化措施)。

    演示:

    1. --创建student_info表的连接条件的索引
    2. create index idx_ci on student_info(course_id);
    1. mysql> explain select * from student_info s
    2. -> inner join course c
    3. -> on s.course_id=c.course_id;
    4. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+------+----------+-------+
    5. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    6. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+------+----------+-------+
    7. | 1 | SIMPLE | c | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100.00 | NULL |
    8. | 1 | SIMPLE | s | NULL | ref | idx_ci | idx_ci | 5 | db1.c.course_id | 9773 | 100.00 | NULL |
    9. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+------+----------+-------+

    虽然主表是s,但实际执行时是c表驱动s表,因为这样可以充分使用到s的索引。

    如果只有c表的连接条件有索引:

    1. drop index idx_ci on student_info; -- 删除s表连接条件的索引
    2. create index idx_ci on course(course_id); -- 创建c表连接条件的索引
    1. mysql> explain select * from student_info s
    2. -> inner join course c
    3. -> on s.course_id=c.course_id;
    4. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
    5. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    6. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
    7. | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using where |
    8. | 1 | SIMPLE | c | NULL | ref | idx_ci | idx_ci | 4 | db1.s.course_id | 1 | 100.00 | NULL |
    9. +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+

    c表如约变为了被驱动表。

    如果两个表都有索引,那就是根据优化器来决定谁是驱动表,谁是被驱动表(会根据数据的多少决定,一般小表会成为驱动表)。

    结论:在内连接中,如果连接字段只能有一个字段有索引,那么被驱动表有索引成本更低;如果都存在索引或都不存在,那么小数据的表作为驱动表成本更低(小表驱动大表)。

    注意:外连接的驱动和被驱动表也不一定是根据SQL语句的顺序决定的,也有可能会被优化器更改。

    3.JOIN语句原理

    join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。

    • 简单嵌套循环连接(Simple Nested-Loop Join)

    从表A中取出一条数据,遍历表B,将匹配到的数据放到结果集,以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:

     此方法性能最低,假设表A有A条数据,表B有B条数据,那么读取的记录数就为A+A*B(这也证明了为什么要小表驱动大表,因为A是驱动表,更能影响读取记录的次数)。

    • 索引嵌套循环连接(Index Nested-Loop Join)

    Index Nested-Loop Join其优化的思路主要是为了减少被驱动表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过驱动表匹配条件直接与被驱动表索引进行匹配,避免和它的的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。

    驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表。

    该方法读取的记录数为A+B(match匹配数)。

    •  块嵌套循环连接(Block Nested-Loop Join)

    Simple Nested-Loop Join中被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录再加载到内存匹配,这样周而复始,大大增加了IO的次数。为了减少被驱动表的IO次数,就出现了Block Nested-Loop Join的方式

    不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。

    1. 通过 show variables like '%optimizer_switch%' 可以查看block_nested_loop是否开启,默认开启。

    2. 驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,通过show variables like '%join_buffer_size%' 查看,容量默认为256K

    4.JOIN小结

    • 整体效率:INLJ>BNLJ>SNLJ
    • 关联查询选择小表驱动大表(度量单位:表行数*每行大小)
    • 尽量给被驱动表的连接条件增加索引
    • 增大join buffer size,使一次缓存的驱动表数据更多,此时被驱动表的扫描次数也就越少
    • 减少驱动表不必要的字段查询,因为字段越少,join buffer所缓存的数据条数就越多

    5.Hash Join

    从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join,默认都会使用hash join。

    • Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。
      • 这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。
      • 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。
      • 它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接(如WHERE A.COL1=B.COL2),这是由Hash的特点决定的。

    二、子查询优化

    子查询查询的效率并不高。原因是:

    • 执行不相关子查询时,会创建一个临时表,临时存储子查询的结果,然后外层查询再从临时表取数据。查询完毕后还需要删除临时表,会消耗过多的IO和CPU资源。
    • 子查询产生的临时表,不论是内存临时表还是磁盘临时表都不存在索引,查询效率不高。
    • 综上两点,临时表越大,对查询性能的影响也越大。

    如果子查询单独查询,返回的结果非常的多,那将导致效率非常低下,甚至内存可能放不下这么多的结果,对于这种情况,MySQL提出了物化表的概念,即将子查询的结果放到一张临时表(也称物化表 select-type为MATERIALIZED)中。

    物化表转连接

    物化表也是一张表,有了物化表后,可以考虑将原本的表和物化表建立连接查询,针对如下的SQL:

    select * from t1 where key1 in (select m1 from s2 where key2 = 'a')

    如果t1表中的key1在物化表中的m1里存在,则加入结果集,对物化表说,如果m1在t1表中的key1里存在,则加入结果集,此时可以将子查询转化为内连接查询,转成连接查询后,就可以享受很多优化措施。

    半连接(semi-join)

    通过物化表可以将子查询转换为连接查询,MySQL在物化表的基础上做了更进一步的优化,即不建立临时表,直接将子查询转为连接查询

    上面的SQL优化后与下面的SQL较为相似:

    select t1.* from t1 inner join s2 on t1.key1 = s2.m1 where s2.key2 = 'a'

    这么一看好像满足可以转换的趋势,不过需要考虑三种情况:

    1. 对于t1表,s2结果集中如果没有满足on条件的,不加入结果集
    2. 对于t1表,s2结果集中有且只有一条符合条件的,加入结果集
    3. 对于t1表,s2结果集中有多条符合条件的,那么该记录将多次加入结果集

    对于情况1、2的内连接,都是符合上面子查询的要求的,但是结果3,在子查询中只会出现一条记录,但是连接查询中将会出现多条,因此二者又不能完全等价,但是连接查询的效果又非常好,因此MySQL推出了半连接(semi-join)的概念。

    对于t1表,只关心s1表中有没有符合条件的记录,而不关心有多少条记录与之匹配,最终的结果集只保留t1表中的就行了,因此MySQL内部的半连接语法类似是这么写的:

    1. select t1.* from t1 semi join s2 on t1.key1 = s2.m1 where s2.key2 = 'a'
    2. --(这不能直接执行,半连接只是一种概念,不开放给用户使用)

    补充:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

    三、排序优化

    在MySQL中支持两种排序方式:

    • FileSort:对数据进行排序,内部或外部排序,占用CPU较多,效率低
    • Index:索引就保证了有序性,不需要再进行排序,效率高

    优化建议:

    1. 可以在ORDER BY子句中使用索引,目的是在ORDER BY子句避免使用FileSort排序。
    2. 尽量使用Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引。
    3. 无法使用Index时,需要对FileSort方式进行调优。

    案例:

    • 情况一:排序字段没有索引时
    1. mysql> explain select * from student_info order by class_id;
    2. +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
    3. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    4. +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
    5. | 1 | SIMPLE | student_info | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using filesort |
    6. +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+

    使用了filesort手动排序,效率不高。

    • 情况二:排序字段有索引,但order by时不limit
    create index idx_cls on student_info(class_id); --创建索引
    1. mysql> explain select * from student_info order by class_id;
    2. +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
    3. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    4. +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
    5. | 1 | SIMPLE | student_info | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using filesort |
    6. +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+

    依然使用的是filesort,为啥呢?

    这是因为如果使用索引排序的话,那么它是二级索引,虽然已经将class_id字段排序好了,但是,因为查询的字段是*,所以还需要疯狂回表,又因为数据量较多(回表操作太多),所以有些情况下还不如直接在聚簇索引的基础上去排序所有数据。

    如果select *改为select class_id,id,排序方式就为index,使用了索引覆盖。

    • 情况三:排序字段有索引,order by时使用limit
    1. mysql> explain select * from student_info order by class_id limit 200;
    2. +----+-------------+--------------+------------+-------+---------------+---------+---------+------+------+----------+-------+
    3. | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    4. +----+-------------+--------------+------------+-------+---------------+---------+---------+------+------+----------+-------+
    5. | 1 | SIMPLE | student_info | NULL | index | NULL | idx_cls | 5 | NULL | 200 | 100.00 | NULL |
    6. +----+-------------+--------------+------------+-------+---------------+---------+---------+------+------+----------+-------+

    加上limit后就使用到了索引,因为优化器认为limit过后数据量较小,回表操作次数也少,就直接使用二级索引排序好的数据了。

    如果limit后的数据过大,照样会使用filesort排序

    • 情况四:联合索引中order by规则不一致
    1. -- 创建联合索引
    2. create index idx_cls_cou on student_info(class_id desc,course_id);

    有多种情况:

    1. -- 索引失效的情况,filesort
    2. explain select * from student_info order by class_id desc,course_id desc limit 20;
    3. explain select * from student_info order by class_id,course_id limit 20;
    4. -- 使用索引的情况,index
    5. explain select * from student_info order by class_id desc,course_id limit 20;
    6. explain select * from student_info order by class_id ,course_id desc limit 20;
    • 情况五:无过滤,不索引
    1. -- 创建联合索引
    2. create index idx_cls_cou on student_info(class_id,course_id);
    3. -- where过滤使用到了class_id索引
    4. explain select * from student_info where class_id=2 order by course_id;
    5. -- 未使用索引
    6. explain select * from student_info where course_id=2 order by class_id;
    7. -- 使用了索引,使用索引排序
    8. explain select * from student_info where course_id=2 order by class_id limit 20;

    小结:

    1. INDEX a_b_c(a,b,c)
    2. order by 能使用索引最左前缀
    3. - ORDER BY a
    4. - ORDER BY a,b
    5. - ORDER BY a,b,c
    6. - ORDER BY a DESC,b DESC,c DESC
    7. 如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
    8. - WHERE a = const ORDER BY b,c
    9. - WHERE a = const AND b = const ORDER BY c
    10. - WHERE a = const ORDER BY b,c
    11. - WHERE a = const AND b > const ORDER BY b,c
    12. 不能使用索引进行排序
    13. - ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
    14. - WHERE g = const ORDER BY b,c /*丢失a索引*/
    15. - WHERE a = const ORDER BY c /*丢失b索引*/
    16. - WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
    17. - WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/

    补充:filesort算法分为双路排序和单路排序。

    双路排序需要加载两次数据(类似回表),单路排序只需要一次(在sort buffer中排序),效率更高,但对内存的要求也更高。

    优化策略:

    1.尝试提高 sort_buffer_size 

    2.尝试提高 max_length_for_sort_data

    3.order by时select需要用到的参数,防止数据超过sort buffer,从而增加额外的操作

    四、GROUP BY优化

    • group by使用索引的原则几乎跟order by一致,group by也可以使用索引。
    • group by是先排序再分组,遵照索引建的最佳左前缀法则
    • 当无法使用索引时,可增大max_length_for_sort_data和sort_buffer_size参数的设置进行优化
    • where效率高于having,能写在where限定的条件就不要写在having中了
    • 减少使用order by,可将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
    • 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果应该比较少,否则SQL会很慢。

    五、优先考虑覆盖索引

    什么是覆盖索引?二级索引能快速找到一个列的数据,但是还需要额外的做一次回表操作,去主键索引查找真正的行数据,但如果要查找列的数据在二级索引中都能找到,那么它不必回表去读取整个行。

    一个二级索引包含了满足查询结果的数据就叫做覆盖索引(常用在联合索引中)。 简单说就是, 索引列+主键包含SELECT需要查找的所有的列。

    即使有些查询看似使用不到索引,但是优化器有些时候会使用到覆盖索引进行优化。因为InnoDB中非聚簇索引查找比聚簇索引消耗更低。(一页存储的数据更多,避免加载多页)

    六、使用前缀索引

    使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。

    七、索引下推ICP

    即回表前再去过滤一些数据,尽可能让需要回表的数据变得更少。如下

    1. create index idx_cls on student_info(class_id);
    2. explain select * from student_info where class_id>10192 and course_id=10038
    • 如果没有索引下推

    mysql使用索引去查找class_id大于10192的数据,然后逐一回表,将所有的这些数据保存起来,然后再根据couse_id找出最后的结果。

    • 如果使用索引下推

    mysql使用索引去查找class_id大于10192的数据,此时不立马回表,继续执行and后面的条件,把结果进一步压缩,再去回表得到最终结果。这样,回表的次数将会大大减小。

    ICP默认是开启的,可以使用系统参数optimizer_switch来控制器是否开启。

    1. -- 修改默认值
    2. set ="index_condition_pushdown=off";
    3. set ="index_condition_pushdown=on";

    ICP的使用条件:

    • 只能用于二级索引
    • explain显示的执行计划中type值(join 类型)为range、ref、eq_ref或者ref_or_null。
    • 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
    • ICP可以用于MyISAM和InnnoDB存储引擎
    • 当SQL使用覆盖索引时,不支持ICP优化方法。

    八、其他查询优化

    1.COUNT(*)与COUNT(具体字段)效率

    在MySQL中统计数据表的行数,可以使用三种方式:SELECT COUNT(*) 、SELECT COUNT(1)和SELECT COUNT(具体字段),使用这三者之间的查询效率是怎样的?

    如果是MyISM存储引擎,那么都是O(1)的复杂度,因为它维持了row_count记录行数值。

    如果是InnoDB存储引擎,需要扫描全表,如果采用COUNT(具体字段)来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT(*)和COUNT(1)来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。

    如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。

    2.不使用SELECT *

    应该是有SELECT 具体字段,因为:

    • 无法使用覆盖索引
    • MySQL在解析的过程中,会通过查询数据字典将"*"按序转换成所有列名,这会大大的耗费资源和时间。
    • 关联查询时join buffer能存的记录数变少

    3.LIMIT 1优化

    针对的是会扫描全表的SQL语句,如果你可以确定结果集只有一条,那么加上LIMIT 1的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

    如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT 1了。
     

    4.多使用commit

    只要有可能,在程序中尽量多使用COMMIT,这样程序的性能得到提高,COMMIT会释放多种资源:

    • 回滚段上用于恢复数据的信息
    • 被程序语句获得的锁
    • redo / undo log buffer中的空间
    • 管理上述3种资源中的内部花费
       

  • 相关阅读:
    【C++】list的介绍及使用 | 模拟实现list(万字详解)
    Java提高与实践
    Elasticsearch的使用
    【毕业设计】大数据用户画像分析系统 - 数据分析
    iOS之crash分析篇--符号化
    制作游戏拼图游戏
    Python入门教程48:Pycharm永久镜像源的pip配置方法
    jetpack compose中实现丝滑的轮播图效果
    【毕业设计】基于单片机的指纹识别门禁系统设计 -物联网 stm32 嵌入式
    湖仓一体全面开启实时化时代
  • 原文地址:https://blog.csdn.net/weixin_45902285/article/details/126042927