避免使用子查询
例:
select * from t1 where id in (select id from t2 where name = 'lolly1023');
其子查询在MySQL5.5版本里,内部执行计划是:先查询外表再匹配内表,而不是先查内表t2,当外表的数据很大时,查询的速度会非常慢。
在MariaDB10/MySQL5.6版本里,采用join关联方式对其进行了优化,这条SQL语句会自动转化为:
select t1.* from t1 left join t2 on t1.id = t2.id where t2.name = "lolly1023";
还有一些原因就是,执行子查询时,会创建临时表,查询完毕之后再删除,所以子查询的速度会收到影响。
join查询的话,小表驱动大表,只适合用较少数据量的适合,如果数据量较大的时候,还是推荐把业务逻辑放到应用层中做关联。
数据来源:探讨MySQL多表查询:使用JOIN还是相关子查询,谁才是合适之举?
用IN来替换OR
- 低效查询
select * from t1 where id = 10086 or id = 580058 or id = 980634;
- 高效查询
select * from t1 where id in (10086,580058,980634);
另外,MySQL对于IN做了响应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。但是如果数值较多,产生的消耗也是比较大的。再例如:
select * from t1 where id in (10086,10087,10088);
对于连续的数值,能用BETWEEN就不要用IN 了;再或者使用连接来替换。
在数据量较少的时候,IN和OR的效率基本一致,非索引字段IN的效率优于OR
- OR的效率为O(n)
- IN的效率为O(logn),当n越大的时候效率相差越明显。
数据来源:or 和 in 的效率对比
读取适当的记录LIMIT M,N,而不要读多余的记录
select * from t1 limit 99952,30;
使用上述SQL做分页的时候,可能会发现,随着表数据量的增加,直接使用limit分页查询会越来越慢。
对于limit m,n的分页查询,越往后面翻页(即m越大的情况下),SQL的耗时会越来越长,对于这种应该先取出主键id,然后通过主键id跟原表进行join关联查询。因为MySQL并不是跳过offset行,而是取offset+N行,然后放弃前offset行,返回N行,那当offset特别大的时候,效率就会非常的地下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。
优化的方法如下:可以取前一夜的最大行数的id(将上次遍历到的最末尾的数据ID传给数据库,然后直接定位到该ID处,再往后遍历数据),然后根据这个最大的id来限制下一页的起点。比如此列中,上一页最大值的id是99952。sql可以采用如下的写法:
select * from t1 where id > 99952 limit 30;
或者使用索引覆盖来优化,可以先查出索引的ID,然后根据ID拿数据,可以采用以下写法:
select * from (select id from t1 limit 99952,30) a left join t1 b on a.id = b.id;
数据来源:MySQL limit使用及超大分页问题解决
禁止非必要的Order By排序
如果我们对结果没有排序的要求,就尽量少使用排序;
如果排序字段没有用到索引,也尽量少用排序;
另外,分组统计查询时可以禁止其默认排序
select s_id,count(*) from t2 group by s_id;
默认情况下,MySQL会对GROUP BY col1,col2的字段进行排序,也就是说上述会对s_id机械能排序,如果想要避免排序结构的消耗,可以指定ORDER BY NULL禁止排序:
select s_id,count(*) from t2 group by s_id order by null;
数据来源:浅析order by中如何处理null以及order by null的作用
总和查询可以禁止排重用union all
union和union all的差异主要是前者需要将结果集合并后再进行唯一性的过滤操作,这就会涉及到排序,增加大量的CPU运算,加大资源消耗及延迟。
当然,union all的前提条件是两个结果集没有重复的数据。所以一般是我们明确知道不会出现重复数据的时候才建议使用union all提高速度。
避免随机取记录
select * from t1 where 1 = 1 order by rand() limit 4;
select * from t1 where id>= ceil(rand()*1000) limit 4;
以上两个语句都无法用到索引
将多次插入换成批量Insert插入
将多次插入:
insert into t(id,name) values(1,'a');
insert into t(id,name) values(2,'b');
insert into t(id,name) values(3,'c');
换成批量插入:
insert into t(id,name) values(1,'a'),(2,'b'),(3,'c');
主要还是因为事务的原因,多次插入的话会有多个事务,但是批量插入的话,只需要一次事务。
数据来源:MySQL批量插入为什么比单条插入快很多
只返回必要的列,用具体的字段列表代替select * 语句
select 语句会增加很多不必要的消耗(CPU,IO,内存,网络带宽);增加了使用覆盖索引的可能性;当表结构发生改变时,前者也需要经常更新。所以要求直接在select*后面加上字段名。
MySQL数据库是按照行的方式存储,而数据库取操作都是以一个页大小进行IO操作的,每个IO单元中存储了多行,每行都是存储了该行的所有字段。所以无论取一个字段还是多个字段,实际上数据库在表中需要访问的数据量其实是一样的。
但是如果查询的字段都在索引中,也就是覆盖索引,那么可以直接从索引中获取对应的内容直接返回,不需要进行回表,减少IO操作。除此之外,每当order by操作的时候,select字句中的字段多少会在很大程度上影响到我们排序的效率。
数据来源:到底为什么不建议使用SELECT * ?
区分IN和EXISTS
select * from t1 where id in (select id from t2);
上面的语句相当于:
select * from t1 where exists(select * from t2 where t1.id = t2.id)
区分in和exists主要是造成了驱动顺序的改变(性能变化的关键),如果是exists,那么以外层表为驱动表先被访问,如果是in,那么先执行子查询。所以in适合于外表大而内表小的情况;exists适合于外表小而内表大的情况。
另外,in查询在某些情况下有可能会查询返回错误的结果,因此,通常是建议在确定且有限的集合时,可以使用in。如in (0,1,2)。
数据来源:in 和 exists的区别
优化Group By语句
如果group by语句的结果没有排序要求,那就在语句后面加上 order by null,因为group by 默认会进行排序;
尽量让group by过程用上表的索引,确认方法是explain结果里没有Using temporary和Using filesort;
- 如果group by需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大tmp_table_size参数,来避免使用到磁盘临时表;
- 如果数据量实在是太大,使用sql_big_result这个提示,来告诉优化器直接使用排序算法(直接用磁盘临时表)得到group by的结果。
使用where替换having字句:避免使用having字句,having只会在检索出所有记录之后才会对结果集进行过滤,这个处理需要排序分组,如果能通过where字句提前进行过滤查询的目数,就可以减少这方面的开销。
- 低效
select id,avg(id) from t2 group by id having id = 100086 or id = 5555;
- 高效
select id,avg(id) from t2 where id = 100086 or id = 5555 group by id;
数据来源:MySQL数据库 group by 语句怎么优化?
尽量使用数字型字段
若只含数值信息的字段尽量不要设计为字符型,这样会降低查询和连接的性能。引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
数据来源:mysql 选择数值还是字符效率高
优化Join语句
当我们执行两个表的Join的时候,就会有一个比较的过程,逐条比较两个表的语句是比较慢的,因此可以把两个表中数据以此读进一个内存块中,在MySQL中执行:
show variables like 'join_buffer_size';
这样可以看到Join在内存中的缓存池大小,其大小将会影响Join语句的性能。在执行Join的时候,数据库会选择一个表把它要返回以及需要进行和其他表进行比较的数据放入join_buffer。
什么是驱动表,什么是被驱动表,这两个概念在查询中有时容易让人搞混,有下面几种情况,大家需要了解。
- 当连接查询没有where条件时:
left join 前面的表是驱动表,后面的表是被驱动表
right join 后面的表是驱动表,前面的表是被驱动表
inner join / join 会自动选择表数据比较少的作为驱动表
straight_join(≈join) 直接选择左边的表作为驱动表(语义上与join类似,但去除了join自动选择小表作为驱动表的特性)
- 当连接查询有where条件时,带where条件的表是驱动表,否则是被驱动表
假设有表如右边:t1与t2表完全一样,a字段有索引,b无索引,t1有100条数据,t2有1000条数据
若被驱动表有索引,那么其执行算法为:Index Nested-Loop Join(NLJ),示例如下:
- 执行语句:select * from t1 straight_join t2 on (t1.a=t2.a);由于被驱动表t2.a是有索引的,其执行逻辑如下:
- 从表t1中读入一行数据 R
- 从数据行R中,取出a字段到表t2里去查找
- 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分
- 重复执行步骤1到3,直到表t1的末尾循环结束
- 如果一条join语句的Extra字段什么都没写的话,就表示使用的是NLJ算法
若被驱动表无索引,那么其执行算法为:Block Nested-Loop Join(BLJ)(Block 块,每次都会取一块数据到内存以减少I/O的开销),示例如下:
- 执行语句:select * from t1 straight_join t2 on (t1.a=t2.b);由于被驱动表t2.b是没有索引的,其执行逻辑如下:
- 把驱动表t1的数据读入线程内存join_buffer(无序数组)中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
- 顺序遍历表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。
- 另外还有一种算法为Simple Nested-Loop Join(SLJ),其逻辑为:顺序取出驱动表中的每一行数据,到被驱动表去做全表扫描匹配,匹配成功则作为结果集的一部分返回。
另外,Innodb会为每个数据表分配一个存储在磁盘的 表名*.ibd* 文件,若关联的表过多,将会导致查询的时候磁盘的磁头移动次数过多,从而影响性能
所以实践中,尽可能减少Join语句中的NestedLoop的循环次数:“永远用小结果集驱动大的结果集”
- 用小结果集驱动大结果集,将筛选结果小的表(在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”)首先连接,再去连接结果集比较大的表,尽量减少join语句中的Nested Loop的循环总次数
- 优先优化Nested Loop的内层循环(也就是最外层的Join连接),因为内层循环是循环中执行次数最多的,每次循环提升很小的性能都能在整个循环中提升很大的性能;
- 对被驱动表的join字段上建立索引;
- 当被驱动表的join字段上无法建立索引的时候,设置足够的Join Buffer Size。
- 尽量用inner join(因为其会自动选择小表去驱动大表).避免 LEFT JOIN (一般我们使用Left Join的场景是大表驱动小表)和NULL,那么如何优化Left Join呢?
- 条件中尽量能够过滤一些行将驱动表变得小一点,用小表去驱动大表
- 右表的条件列一定要加上索引(主键、唯一索引、前缀索引等),最好能够使type达到ange及以上(ref,eq_ref,const,system)
- 适当地在表里面添加冗余信息来减少join的次数
- 使用更快的固态硬盘
性能优化,left join 是由左边决定的,左边一定都有,所以右边是我们的关键点,建立索引要建在右边。当然如果索引是在左边的,我们可以考虑使用右连接,如下:
-- 最好在bid上建索引
select * from atable left join btable on atable.aid=btable.bid;
Tips:Join左连接在右边建立索引;组合索引则尽量将数据量大的放在左边,在左边建立索引。
数据来源:如何进行JOIN优化?