嵌套循环join:实际上就是简单粗暴的嵌套循环,如果table1有1万条数据,table2有1万条数据,那么数据比较的次数=1万 * 1万 = 1亿次,这种查询效率会非常慢。
缓存块嵌套循环join:通过一次性缓存多条数据,把参与查询的列缓存到Join Buffer 里,然后拿join buffer里的数据批量与内层表的数据进行匹配,从而减少了内层循环的次数(遍历一次内层表就可以批量匹配一次Join Buffer里面的外层表数据)。
扫描次数计算公式:
(S* C)/join_buffer_size + 1
使用join buffer的条件:
join_buffer-size
变量设置join buffer大小show variables like 'join_buffer_size'; set global join_buffer_size = 1024*1024*50;
explain查询结果中,Extra 列出现 'Using join buffer (Block Nested Loop)' 表示使用了BNLJ。
批量键值访问,由MySQL5.6引入,BKA的基石: Multi Range Read ( MRR )
什么是MRR?
select * from salaries where from_date <='1980-01-01';
这条语句的查询结果可能会产生随机IO,应为数据是按照主键排列,而不是from_date字段排列的。如果开启MRR,那么会读取结果的主键索引,并按照主键索引排序,排序完成后再到表数据里读取数据,这样读出来的数据就是顺序IO,性能会比随机IO好很多。
: MRR核心:将随机IO转换成顺序1O,从而提升性能。
MRR参数:
show variables like '%optimizer_switch%'; show variables like '%read_rnd_buffer_size%'; set optimizer_switch ='mrr_cost_based=off';
一旦开启MRR,会在extra里面展示Using MRR
BKA流程;
参数:
-- 默认是关闭的,开启语句: set optimizer_switch ='batched_key_access=on';
当使用BKA的时候,会在extra里面展示Using join buffer (Batched Key Access)
注意:
1、用小表驱动大表
看上图,先执行的就是驱动表,explin的结果就是按执行顺序展示的。
注:用小表驱动大表一般无需人工考虑,MySQL关联查询优化器会自动选择最优的执行顺序。如果优化器抽风失效,可使用STRAIGHT-JOIN,强制先读取左边的表,再读取右边的表。
2、如果有where条件,应当要能够使用索引,并尽可能地减少外层循环的数据量
3、join的字段尽量创建索引
注意:当join字段的类型不同时,索引无法使用
4、尽量减少扫描的行数(explain-rows)
5、参与join的表不要太多
阿里编程规约建议不超过3张。如果业务需要join多张的表,可以根据代码逻辑适当拆分。
6、如果被驱动表的join字段用不了索引,且内存较为充足,可以考虑把join buffer设置得大一些。
有这样一条sql
select * from employees limit 300000,10;
查询第一页的时候,花费92ms,查询第300001页的时候,花费174ms
方案1:覆盖索引 (108ms)
select emp_no from employees limit 300000,10;
方案2:覆盖索引+join(109ms)
select * from employees e inner join (select emp_no from employees limit 300000,10) t on e.emp_no = t.emp_no; -- 连接字段都叫emp_no,这行可以简写成 using (emp_no);
方案3:覆盖索引+子查询(126ms)
select * from employees where emp_no >= (select emp_no from employees limit 300000,1) limit 10;
方案4:范围查询+limit语句
select * from employees limit 10; select * from employees where emp_no > 10010 limit 10;
本例是拿第二页数据,首先查询第一页数据,拿到最后一个数据id值比如说是10010,再根据这个id值去拿第二页数据。这种方式扫描的行数永远都只有10行。
方案5:如果能获得起始主键值 & 结束主键值
select * from employees where emp_no between 20000 and 20010;
方案6:禁止传入过大的页码
从业务层面解决。
为什么?
innodb非主键索引:叶子节点存储的是:索引+主键;
主键索引叶子节点:主键+表数据;
在1个page里面,非主键索引可以存储更多的条目。
count(字段)只会针对该字段统计,使用这个字段上面的索引(如果有的话),并且会排除掉该字段值为null的行,而count(*)不会排除。
count()和count(1)没有区别。
对于MyISAM引擎,如果count()没有where条件(形如 select count() from 表名),查询会非常的快。
对于MySQL 8.0.13,InnoDB引擎,如果count()没有where条件(形如 select count(*) from 表名),查询也会被优化,性能有所提升。
方案1:创建一个更小的非主键索引
方案2:把数据库引擎换成MyISAM => 实际项目用的很少,一般不会修改数据库引擎
方案3:创建汇总表 table[table_name, count]
方案4:缓存 select count(*) 结果存放到缓存
方案5:information_schema.tables
select * from `information_schema`.TABLES where TABLE_SCHEMA = '库名' and TABLE_NAME = '表名'; -- 好处:不操作salaries表,不论salaries有多少数据,都可以迅速地返回结果 -- 缺点:估算值,并不是准确值
show table status where Name = 'salaries'; -- 好处:不操作salaries表,不论salaries有多少数据,都可以迅速地返回结果 -- 缺点:估算值,并不是准确值
explain select * from salaries; -- 好处:不操作salaries表,不论salaries有多少数据,都可以迅速地返回结果 -- 缺点:估算值,并不是准确值
最好的做法是:利用索引避免排序,利用索引本身的有序性,让MySQL跳过排序过程
下面做个实验,employees表有组合索引index(first_name, last_name):
竟然是全表扫描,没有使用索引,我们在来给语句加个limit来看下
有变成了index,使用上了索引,这是为什么呢?
因为当MySQL优化器发现全表扫描开销比使用索引的开销更低时,会直接用全表扫描。
那这条语句到底能不能使用索引避免排序呢?我们来看下Extra的值:
Extra是null,说明是可以使用索引避免排序的,如果Extra是using filesort时,说明不可以。
按照上面这种方法,我又试了好几种排序语句,列在下面并给出结论:
/* * 可以使用索引避免排序 * [Bader,last_name1, emp_no] * [Bader,last_name2, emp_no] * [Bader,last_name3, emp_no] * [Bader,last_name4, emp_no] * [Bader,last_name5, emp_no] * .. */ explain select * from employees where first_name = 'Bader' order by last_name; /* * 可以使用索引避免排序 * ['Angel', lastname1, emp_no1] * ['Anni', lastname1, emp_no1] * ['Anz', lastname1, emp_no1] * ['Bader', lastname1, emp_no1] */ explain select * from employees where first_name < 'Bader' order by first_name; /* * 可以使用索引避免排序 */ explain select * from employees where first_name = 'Bader' and last_name > 'Peng' order by last_name; /* * 无法利用索引避免排序【排序字段存在于多个索引中】 * - first_name => (first_name,last_name) * - emp_no => 主键 */ explain select * from employees order by first_name, emp_no limit 10; /* * 无法利用索引避免排序【升降序不一致】 */ explain select * from employees order by first_name desc, last_name asc limit 10; /* * 无法利用索引避免排序【使用key_part1范围查询,使用key_part2排序】 * ['Angel', lastname1, emp_no1] * ['Anni', lastname1, emp_no1] * ['Anz', lastname1, emp_no1] * ['Bader', lastname1, emp_no1] */ explain select * from employees where first_name < 'Bader' order by last_name;
利用索引,防止filesort的发生
如果发生了filesort,并且没办法避免,想办法优化filesort
SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on; SET optimizer_trace_offset=-30, optimizer_trace_limit=30; select * from employees where first_name < 'Bader' order by last_name; select * from `information_schema`.OPTIMIZER_TRACE where QUERY like '%Bader%';
show status like '%sort_merge_passes%'
group by 语句优化有三种方式,分别是(性能从高到低):
松散索引扫描( Loose Index Scan ) > 紧凑索引扫描( Tight Index Scan) > 临时表( Temporary table )
无需扫描满足条件的所有索引键即可返回结果。
explain的extra展示Using index for group-by 说明使用了松散索引扫描。
使用条件:
查询作用在单张表上
GROUP指定的所有字段要符合最左前缀原则,且没有其他字段
如果存在聚合函数,只支持MINO/MAX) ,并且如果同时使用了.MIN()和MAX),则必须作用在同一个字段。聚合函数作用的字段必须在索引中,并且要紧跟GROUP BY所指定的字段
如果查询中存在除GROUP BY指定的列以外的其他部分,则必须以常量的形式出现
索引必须索引整个字段的值,不能是前缀索引
举例:
假设有index(c1,c2,c3)作用在表t1 (c1,c2,c3,c4)上,下面这些SQL都能使用松散索引扫描:
SELECT c1, c2 FROM t1 GROUP BY c1, c2; SELECT DISTINCT c1, c2 FROM t1;SELECT C1, MIN (c2) FROM t1 GROUP BY c1; SELECT c1, c2 FROM t1 WHERE c1 < const GROUP BY cl, c2; SELECT MAX(c3), MIN (c3), cl, c2 FROM t1 WHERE c2 > const GROUP BY cl, c2; SELECT c2 FROM t1 WHERE c1 < const GROUP BY cl, c2; SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2;
下面这些SQL不能使用松散索引扫描:
-- 聚合函数不是MIN()或MAX() SELECT c1, SUM(c2) FROM t1 GROUP BY c1; -- 不符合最左前缀则 SELECT c1, c2 FROM t1 GROUP BY c2, c3; -- 查询了c3字段,但是c3字段上没有等值查询 -- 改成 SELECT c1, c3 FROM t1 WHERE c3 = const GROUP BY c1, c2; 则可以使用 SELECT c1, с3 FROM t1 GROUP BY с1, c2;
需要扫描满足条件的所有索引键才能返回结果,性能一般比松散索引扫描差,但一般都可接受。
explain-extra没有明显的标识。
紧凑索引扫描也没有办法使用的话, MySQL将会读取需要的数据,并创建一个临时表,用临时表实现GROUP BY操作。
explain-extra显示Using temporary。
如果发现group by语句出现临时表,可是适当加索引,让其使用松散索引扫描或紧凑索引扫描。
遵循三范式可以很好的防止冗余。
字段具有原子性,即数据库表的每一个字段都是不可分割的原子数据项,不能是集合、数组、记录等非原子数据项,当实体中的某个属性有多个值时,必须拆分为不同的属性。
例如:
不符合第一范式,address字段还可以拆分成省、市、区等
修改:
满足第一范式的基础上,要求每一行数据具有唯一性,并且非主键字段完全依赖主键字段。
例如:
不符合第二范式,课程学分字段不依赖于学号字段,而依赖于课程字段,只依赖了部分主键
修改:
满足第二范式的基础上,不能存在传递依赖。
例如:
不符合第三范式,学校地址和学校电话字段依赖了学校字段,学校字段又依赖主键字段,存在传递依赖
修改:
有时为了提升查询效率,我们也会不遵从三范式,适当增加冗余。
例如下面的两张表
这两张表是符合三范式的,假设两张表的数据量非常大,而业务要求每次查询学生信息时都要携带学校地址,这种情况下,我们就可以在学生表里冗余存储学校地址,这样就不用联表查询,提升效率。
字段少而精,建议20个以内(经验之谈),超过可以拆分
尽量用小型字段,
避免使用允许为NULL的字段
合理平衡范式与冗余
如果数据量非常大,考虑分库分表