数据库调优的方式有多种:
大方向上可以分为物理查询优化和逻辑查询优化两块。
数据准备:
-
- CREATE TABLE `student_info` (
- `id` int NOT NULL AUTO_INCREMENT,
- `student_id` int NOT NULL,
- `name` varchar(20) DEFAULT NULL,
- `course_id` int NOT NULL,
- `class_id` int DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8;
- CREATE TABLE `course` (
- `id` int NOT NULL AUTO_INCREMENT,
- `course_id` int NOT NULL,
- `course_name` varchar(40) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;
- mysql> explain select * from student_info s
- -> left join course c
- -> on s.course_id=c.course_id;
- +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
- | 1 | SIMPLE | c | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100.00 | NULL |
- | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 10.00 | Using where; Using join buffer (hash join) |
- +----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+--------------------------------------------+
可以看到驱动表和被驱动表都没有索引(ALL),都是全表进行扫描。
此时连接查询就相当于一个双层循环,驱动表的每一次循环都会去遍历被驱动表,消耗比较大。但查询优化器会进行优化,被驱动表的Extra字段是Using join buffer (hash join),后续会讲。
执行计划中的第一条记录是驱动表,其余的都是被驱动表
create index idx_ci on course(course_id); --给被驱动表的连接条件添加索引
- mysql> explain select * from student_info s
- -> left join course c
- -> on s.course_id=c.course_id;
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
- | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using where |
- | 1 | SIMPLE | c | NULL | ref | idx_ci | idx_ci | 4 | db1.s.course_id | 1 | 100.00 | NULL |
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
此时被驱动表的type为ref,使用到了索引,加快了被驱动表的搜索。(此例中给驱动表加索引是没有意义的,因为要全表数据)
内连接和外连接没有索引时是相同的,都使用join buffer加快连接速度,但是内连接的特性是主表和从表地位是相同的,位置可以改变,所以优化器会根据情况优化修改两者的位置(所以我们也可以将外连接转内连接,这样sql语句可以享受更多的优化措施)。
演示:
- --创建student_info表的连接条件的索引
- create index idx_ci on student_info(course_id);
- mysql> explain select * from student_info s
- -> inner join course c
- -> on s.course_id=c.course_id;
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+------+----------+-------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+------+----------+-------+
- | 1 | SIMPLE | c | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100.00 | NULL |
- | 1 | SIMPLE | s | NULL | ref | idx_ci | idx_ci | 5 | db1.c.course_id | 9773 | 100.00 | NULL |
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+------+----------+-------+
虽然主表是s,但实际执行时是c表驱动s表,因为这样可以充分使用到s的索引。
如果只有c表的连接条件有索引:
- drop index idx_ci on student_info; -- 删除s表连接条件的索引
- create index idx_ci on course(course_id); -- 创建c表连接条件的索引
- mysql> explain select * from student_info s
- -> inner join course c
- -> on s.course_id=c.course_id;
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
- | 1 | SIMPLE | s | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using where |
- | 1 | SIMPLE | c | NULL | ref | idx_ci | idx_ci | 4 | db1.s.course_id | 1 | 100.00 | NULL |
- +----+-------------+-------+------------+------+---------------+--------+---------+-----------------+--------+----------+-------------+
c表如约变为了被驱动表。
如果两个表都有索引,那就是根据优化器来决定谁是驱动表,谁是被驱动表(会根据数据的多少决定,一般小表会成为驱动表)。
结论:在内连接中,如果连接字段只能有一个字段有索引,那么被驱动表有索引成本更低;如果都存在索引或都不存在,那么小数据的表作为驱动表成本更低(小表驱动大表)。
注意:外连接的驱动和被驱动表也不一定是根据SQL语句的顺序决定的,也有可能会被优化器更改。
join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。
从表A中取出一条数据,遍历表B,将匹配到的数据放到结果集,以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:

此方法性能最低,假设表A有A条数据,表B有B条数据,那么读取的记录数就为A+A*B(这也证明了为什么要小表驱动大表,因为A是驱动表,更能影响读取记录的次数)。
Index Nested-Loop Join其优化的思路主要是为了减少被驱动表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过驱动表匹配条件直接与被驱动表索引进行匹配,避免和它的的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。

驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表。
该方法读取的记录数为A+B(match匹配数)。
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
从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join,默认都会使用hash join。
子查询查询的效率并不高。原因是:
如果子查询单独查询,返回的结果非常的多,那将导致效率非常低下,甚至内存可能放不下这么多的结果,对于这种情况,MySQL提出了物化表的概念,即将子查询的结果放到一张临时表(也称物化表 select-type为MATERIALIZED)中。
物化表也是一张表,有了物化表后,可以考虑将原本的表和物化表建立连接查询,针对如下的SQL:
select * from t1 where key1 in (select m1 from s2 where key2 = 'a')
如果t1表中的key1在物化表中的m1里存在,则加入结果集,对物化表说,如果m1在t1表中的key1里存在,则加入结果集,此时可以将子查询转化为内连接查询,转成连接查询后,就可以享受很多优化措施。
通过物化表可以将子查询转换为连接查询,MySQL在物化表的基础上做了更进一步的优化,即不建立临时表,直接将子查询转为连接查询。
上面的SQL优化后与下面的SQL较为相似:
select t1.* from t1 inner join s2 on t1.key1 = s2.m1 where s2.key2 = 'a'
这么一看好像满足可以转换的趋势,不过需要考虑三种情况:
对于情况1、2的内连接,都是符合上面子查询的要求的,但是结果3,在子查询中只会出现一条记录,但是连接查询中将会出现多条,因此二者又不能完全等价,但是连接查询的效果又非常好,因此MySQL推出了半连接(semi-join)的概念。
对于t1表,只关心s1表中有没有符合条件的记录,而不关心有多少条记录与之匹配,最终的结果集只保留t1表中的就行了,因此MySQL内部的半连接语法类似是这么写的:
- select t1.* from t1 semi join s2 on t1.key1 = s2.m1 where s2.key2 = 'a'
- --(这不能直接执行,半连接只是一种概念,不开放给用户使用)
补充:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代
在MySQL中支持两种排序方式:
优化建议:
案例:
- mysql> explain select * from student_info order by class_id;
- +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
- | 1 | SIMPLE | student_info | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using filesort |
- +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
使用了filesort手动排序,效率不高。
create index idx_cls on student_info(class_id); --创建索引
- mysql> explain select * from student_info order by class_id;
- +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
- | 1 | SIMPLE | student_info | NULL | ALL | NULL | NULL | NULL | NULL | 996875 | 100.00 | Using filesort |
- +----+-------------+--------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
依然使用的是filesort,为啥呢?
这是因为如果使用索引排序的话,那么它是二级索引,虽然已经将class_id字段排序好了,但是,因为查询的字段是*,所以还需要疯狂回表,又因为数据量较多(回表操作太多),所以有些情况下还不如直接在聚簇索引的基础上去排序所有数据。
如果select *改为select class_id,id,排序方式就为index,使用了索引覆盖。
- mysql> explain select * from student_info order by class_id limit 200;
- +----+-------------+--------------+------------+-------+---------------+---------+---------+------+------+----------+-------+
- | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
- +----+-------------+--------------+------------+-------+---------------+---------+---------+------+------+----------+-------+
- | 1 | SIMPLE | student_info | NULL | index | NULL | idx_cls | 5 | NULL | 200 | 100.00 | NULL |
- +----+-------------+--------------+------------+-------+---------------+---------+---------+------+------+----------+-------+
加上limit后就使用到了索引,因为优化器认为limit过后数据量较小,回表操作次数也少,就直接使用二级索引排序好的数据了。
如果limit后的数据过大,照样会使用filesort排序
- -- 创建联合索引
- create index idx_cls_cou on student_info(class_id desc,course_id);
有多种情况:
- -- 索引失效的情况,filesort
- explain select * from student_info order by class_id desc,course_id desc limit 20;
- explain select * from student_info order by class_id,course_id limit 20;
- -- 使用索引的情况,index
- explain select * from student_info order by class_id desc,course_id limit 20;
- explain select * from student_info order by class_id ,course_id desc limit 20;
-
- -- 创建联合索引
- create index idx_cls_cou on student_info(class_id,course_id);
- -- where过滤使用到了class_id索引
- explain select * from student_info where class_id=2 order by course_id;
- -- 未使用索引
- explain select * from student_info where course_id=2 order by class_id;
- -- 使用了索引,使用索引排序
- explain select * from student_info where course_id=2 order by class_id limit 20;
小结:
- INDEX a_b_c(a,b,c)
- order by 能使用索引最左前缀
- - ORDER BY a
- - ORDER BY a,b
- - ORDER BY a,b,c
- - ORDER BY a DESC,b DESC,c DESC
- 如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- - WHERE a = const ORDER BY b,c
- - WHERE a = const AND b = const ORDER BY c
- - WHERE a = const ORDER BY b,c
- - WHERE a = const AND b > const ORDER BY b,c
- 不能使用索引进行排序
- - ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */
- - WHERE g = const ORDER BY b,c /*丢失a索引*/
- - WHERE a = const ORDER BY c /*丢失b索引*/
- - WHERE a = const ORDER BY a,d /*d不是索引的一部分*/
- - 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,从而增加额外的操作
什么是覆盖索引?二级索引能快速找到一个列的数据,但是还需要额外的做一次回表操作,去主键索引查找真正的行数据,但如果要查找列的数据在二级索引中都能找到,那么它不必回表去读取整个行。
一个二级索引包含了满足查询结果的数据就叫做覆盖索引(常用在联合索引中)。 简单说就是, 索引列+主键包含SELECT需要查找的所有的列。
即使有些查询看似使用不到索引,但是优化器有些时候会使用到覆盖索引进行优化。因为InnoDB中非聚簇索引查找比聚簇索引消耗更低。(一页存储的数据更多,避免加载多页)
使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
即回表前再去过滤一些数据,尽可能让需要回表的数据变得更少。如下
- create index idx_cls on student_info(class_id);
-
- 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来控制器是否开启。
- -- 修改默认值
- set ="index_condition_pushdown=off";
- set ="index_condition_pushdown=on";
ICP的使用条件:
在MySQL中统计数据表的行数,可以使用三种方式:SELECT COUNT(*) 、SELECT COUNT(1)和SELECT COUNT(具体字段),使用这三者之间的查询效率是怎样的?
如果是MyISM存储引擎,那么都是O(1)的复杂度,因为它维持了row_count记录行数值。
如果是InnoDB存储引擎,需要扫描全表,如果采用COUNT(具体字段)来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT(*)和COUNT(1)来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。
如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
应该是有SELECT 具体字段,因为:
针对的是会扫描全表的SQL语句,如果你可以确定结果集只有一条,那么加上LIMIT 1的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT 1了。
只要有可能,在程序中尽量多使用COMMIT,这样程序的性能得到提高,COMMIT会释放多种资源: