【优化总结口诀】
全值匹配要记牢,最左前缀不能忘;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
Like百分写最右,覆盖索引不写星;
不等空值还有or,索引失效要少用;
引号不可丢,丢了就失效。
在讲解之前,先创建两个表:
CREATE TABLE IF NOT EXISTS `class` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `card` INT(10) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE IF NOT EXISTS `book` ( `bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `card` INT(10) UNSIGNED NOT NULL, PRIMARY KEY (`bookid`) );
插入数据:
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20))); INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
查询 class.card = book.card 的所有 class:
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;
可以看到类型为 all,接下来我们建立索引,对其进行优化:此时只需要 card 的索引即可。那么是添加左边的表的card为索引?还是添加右边的表呢?
添加右边的表 book 的card为索引
ALTER TABLE `book` ADD INDEX idx_card( `card`);
查询结果如下:
添加左边的表 class的card为索引
在此之前,应该先删除已有的 book 索引::drop index idx_card on book;
添加 class索引:
ALTER TABLE `book` ADD INDEX idx_card( `card`);
查询结果如下:
总结:
① 在优化关联查询时,只有在被驱动表上建立索引才有效!
②left join 时,左侧的为驱动表,右侧为被驱动表!
运行下面两条查询语句观察结果:
EXPLAIN SELECT * FROM book inner join class on class.card=book.card; 和 EXPLAIN SELECT * FROM class inner join book on class.card=book.card;
结果如下:
结论1: 可以看到两个查询字段调换顺序,结果是一样的!
在 book 表中,删除 9 条记录 然后查询结果:
结论2: :inner join 时,mysql 会自己帮你把小结果集的表选为驱动表,将大结果集的表设置为非驱动表。在非驱动表上加索引才有用
使用 straight_join 观察结果:
结论3: :straight_join::效果和 inner join 一样,但是会强制将左侧作为驱动表!
场景1:
先查询再左联(将子查询部分驱动表位置)
EXPLAIN SELECT ed.name '人物',c.name '掌门' FROM (SELECT e.name,d.ceo from t_emp e LEFT JOIN t_dept d on e.deptid=d.id) ed LEFT JOIN t_emp c on ed.ceo= c.id;
先左联在查询(将子查询部分放入了被驱动表中)
EXPLAIN SELECT e.name '人物',tmp.name '掌门' FROM t_emp e LEFT JOIN (SELECT d.id did,e.name FROM t_dept d LEFT JOIN t_emp e ON d.ceo=e.id)tmp ON e.deptId=tmp.did;
上述两个案例,第一个查询效率较高,且有优化的余地。第二个案例中,子查询作为被驱动表,由于子查询是虚表,无法建立索引,因此不能优化。
结论:
1、子查询尽量不要放在被驱动表,有可能使用不到索引;
2、left join时,尽量让实体表作为被驱动表。
场景2:
不用子查询,直接左联
EXPLAIN SELECT e1.name '人物',e2.name '掌门' FROM t_emp e1 LEFT JOIN t_dept d on e1.deptid = d.id LEFT JOIN t_emp e2 on d.ceo = e2.id ;
使用子查询
Explain SELECT e2.name '人物', (SELECT e1.name FROM t_emp e1 where e1.id= d.ceo) '掌门' from t_emp e2 LEFT JOIN t_dept d on e2.deptid=d.id;
结论: 能够直接多表关联的尽量直接关联,不用子查询!
原场景:
取所有不为掌门人的员工,按年龄分组!
select age as '年龄', count(*) as '人数' from t_emp where id not in (select ceo from t_dept where ceo is not null)
结果:
优化方案1:
解决 dept 表的全表扫描,建立 ceo 字段的索引:
进一步优化:
去除子查询,改为多表关联
select age as '年龄',count(*) as '人数' from emp e left join dept d on e.id=d.ceo where d.id is null group by age;
where 条件和 on 的判断这些过滤条件,作为优先优化的部门,是要被先考虑的!其次,如果有分组和排序,那么也要考虑 grouo by 和 order by。
MySQL 支持二种方式的排序 FileSort 和 Index。Index(使用已建立好的索引直接查找即可)效率高.它指MySQL扫描索引本身完成排序。FileSort(使用快排)方式效率较低。
ORDER BY满足两情况,会使用Index方式排序:
无过滤,不索引。where,limt 都相当于一种过滤条件,只有拥有这些过滤条件才能使用上索引!
实例: 创建索引
create index idx_age_deptid_name on emp (age,deptid,name);
有 where 的查询语句 : explain select * from emp where age=40 order by deptid;
无 where 的查询语句 : explain select * from emp where age=40 order by deptid;
无limit 的查询语句: explain select * from emp order by age,deptid;
有 limit 的查询语句: explain select * from emp order by age,deptid limit 10;
结论1: 当 排序中存在为建立索引的字段,会产生 filesort
索引中都有的字段: ①explain select * from emp where age=45 order by deptid,name;
出现 empno 这种索中没有的字段时:②explain select * from emp where age=45 order by deptid,empno;
结论2: where 两侧列的顺序可以变换,效果相同,但是 order by 列的顺序不能随便变换!
结论3: 最左匹配。必须先 age 再id。顺序错,还是会调用 filesort。
在order by 是都选择正序或者逆序都可以直接使用 index 树:
①explain select * from emp where age=45 order by deptid desc, name desc ;
在 orderby 时,使用相反的排序方法(一个正序一个逆序)就会造成 filesort
②explain select * from emp where age=45 order by deptid asc, name desc ;
①首先,清除 emp 上面的所有索引,只保留主键索引!
drop index idx_age_deptid_name on emp;
②查询:年龄为 30 岁的,且员工编号小于 101000 的用户,按用户名称排序。
explain SELECT SQL_NO_CACHE * FROM emp WHERE age =30 AND empno <101000 ORDER BY NAME;
此时没有索引,使用了 filesort
③全表扫描肯定是不被允许的,因此我们要考虑优化。
思路: 首先需要让 where 的过滤条件,用上索引;
查询中,age 和 empno 是查询的过滤条件,而 name 则是排序的字段,因此我们来创建一个此三个字段的复合索引:
create index idx_age_empno_name on emp(age,empno,name);
empno 是范围查询,因此导致了索引失效,所以 name 字段仍然无法使用索引排序。
由于所有的排序都是在条件过滤之后才执行的,所以此时 empno 的范围查询和索引排序中只能有一个能够使用索引。
结论: 当范围条件和 group by 或者 order by 的字段出现二选一时 ,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。 反之,亦然。
①双路排序
MySQL 4.1 之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据:
②:单路排序
由于单路是后出的,总体而言好过双路。但是存在以下问题:
为了能够减少 IO 的次数,我们可以通过下面这些操作进行调节:
sort_buffer_size
参数的设置。增大缓存的大小,自然可以使一次 IO 读取更多条数据进行操作。两种算法的数据都有可能超出 sort_buffer 的容量,超出之后,会创建 tmp 文件进行合并排序,导致多次 I/O,但是用单路排序算法的风险会更大一些, 所以要提高sort_buffer_size。max_length_for_sort_data
参数的设置:由于排序需要一定的内存空间,因此每次排序的条数必须小于 max_length_for_sort_data
。该参数值一定小于 sort_butter_size
。通过增加一次性排序的条数,保证一个 sort_buffer
中的数据能够更快排序完成。max_length_for_sort_data
而且排序字段不是 TEXT|BLOB 类型时,会用改进后的SQL 只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据。
group by 使用索引的原则几乎跟 order by 一致 ,唯一区别是 groupby 即使没有过滤条件用到索引,也可以直接使用索引。
select e1.name empname,e1.age empage,e2.name ceoname,e2.age ceoage # 内联,找到有门派的所有成员(内联不包含没有门派的成员) from t_emp e1 inner join t_dept d on e1.deptid=d.id # 内联:找到每个门派对应的掌门的具体信息 inner join t_emp e2 on d.ceo=e2.id # 判断过滤 where e1.age>e2.age;
如上所示,两个被驱动表都用上了索引。
思路: 先取门派的平均年龄,再跟自己的年龄做对比!
select e1.name from t_emp e1 inner join (select deptid,AVG(age) avgage from t_emp# 内联一个虚表tmp,通过子查询获得 group by deptid) tmp on e1.deptid=tmp.deptid # 找到 每位成员与之对应的门派 where e1.age<tmp.avgage;# 筛选
结果显示(大表结果):
可以看到此时没有建立索引,type 为 all
索引优化:
①:首先我们必须先确定谁是驱动表谁是被驱动表:
因为是 inner join,因此会自动将小表作为驱动表,也就是说,分组后的 tmp 是驱动表,而 e1 是被驱动表;由于子查询不为被驱动表,因此可以在被驱动表上进行建立索引进行优化
②:由于该语句主要使用了 deptid 字段,因此需要建立 deptid 为索引。
③而在 e1 中,需要查询 deptid 和 age 两个字段,因此这两个字段也需要建立索引
总结 :创建 deptid 和 age 的符合索引: create index idx_deptid_age e on emp(deptid,age);
建立索引的结果如下:
思路: 先查询大于 40 岁的成员,然后按照门派分组,然后再判断至少有 2 个的门派!
select d.deptName,count(*) from t_emp e inner join t_dept d on e.deptid=d.id where e.age>40 group by d.id,d.deptName having count(*)>=2
优化:
①两表关联,我们可以考虑将小表作为驱动表(即dept 为驱动表,emp 为被驱动表)。
②group by 的字段 id,deptName 还可以建立索引: create index idx_id_deptName on dept(id,deptName);
③被驱动表的 deptid 作为关联字段,可以建立索引:create index idx_deptid on emp(deptid);
create index idx_id_deptname on dept(id,deptName);
思路:先找所有成员与之对应的门派,然后将这些成员与门派信息进行左联。只有是掌门人的成员具有门派信息。不是掌门人的成员的门派信息位 null。找到这些null,进行分组和判断。
select d2.deptName from t_emp e inner join t_dept d2 on e.deptid=d2.id left join t_dept d on e.id=d.ceo where d.id is null and e.deptid is not null group by d2.deptName,d2.id having count(*)>=2;
优化分析: 三个表关联,然后做 group by 分组!
①group by 的字段,可以加上索引:create index idx_deptname_id on dept(deptName,id);
②可以将部门表作为驱动表
③第一次 join 时(内联小表驱动大表),e 表作为被驱动表,可以将 deptid 设置索引:create index idx_deptid on emp(deptid);
④最有一次 join 中(左联,左边驱动右边驱动),使用了 dept 表作为被驱动表,查询 ceo 字段,因此可以在 ceo 上面建立索引create index idx_ceo on dept(ceo);
思路: 使用左联,然后显示所有成员的门派情况。(是掌门才显示门派情况,不是就显示 null)
select e.name,case when d.id is null then '否' else '是' end '是否为掌门' from t_emp e left join t_dept d on e.id=d.ceo;
索引优化:
在被驱动表 t_dept 中的 ceo 上建立索引即可。
思路: 先从 emp 表求出,各门派的平均年龄,分组,然后在关联 dept 表,使用 if 函数进行判断!
select d.deptName,if(avg(age)>40,'老鸟','菜鸟') from t_emp e inner join t_dept d on d.id=e.deptid group by d.deptName,d.id
优化:
①使用 dept 作为驱动表
②在 dept 上建立 deptName 和 id 的索引:create index idx_deptName_id on dept(deptName,id);
③在 emp 上建立 deptid 字段的索引: create index index_deptid on emp(deptid);
思路: 先查询所有的员工,根据员工的 deptid 进行分组,找到每组中的最大年龄,形成新表 tmp(包含 depid,和当前最大年龄)。将 员工表 和 tmp 表内联,以depid和age同时相等为条件。最后就能获得想要的表。
select * from T_emp e inner join (select deptid,max(age) maxage from t_emp group by deptid) tmp on e.deptid = tmp.deptid and e.age=tmp.maxage
优化思路:
①子查询中,emp 表根据 deptid 进行分组,因此可以建立 deptid 字段的索引;
②inner join 查询中,关联了 age 和 deptid,因此可以在 deptid,age 字段建立索引
create index idx_deptid_age on emp(deptid,age);