今天遇到一个left join优化的问题,搞了一下午,中间查了很多资料,对MySQL的查询计划还有查询优化有了更进一步的了解,作一个简单的记录:
select c.* from hotel_info_original c
left join hotel_info_collection h
on c.hotel_type=h.hotel_type and c.hotel_id =h.hotel_id
where h.hotel_id is null
这个sql是用来查询出c表中有h表中无的记录,因此想到了用left join的特性(返回左边所有记录,右表不知足匹配条件的记录对应行返回null)来知足需求,不料这个查询很是慢。先来看查询计划:
rows表明这个步骤相对上一步结果的每一行须要扫描的行数,能够看到这个sql须要扫描的行数为35773*8134,很是大的一个数字。原本c和h表的记录条数分别为40000+和10000+,这几乎是两个表作笛卡尔积的开销了(select * from c,h)。
因而我上网查了下MySQL实现join的原理,原来MySQL内部采用了一种叫作 nested loop join的算法。Nested Loop Join 实际上就是经过驱动表的结果集做为循环基础数据,而后一条一条的经过该结果集中的数据做为过滤条件到下一个表中查询数据,而后合并结果。若是还有第三个参与 Join,则再经过前两个表的 Join 结果集做为循环基础数据,再一次经过循环查询条件到第三个表中查询数据,如此往复,基本上MySQL采用的是最容易理解的算法来实现join。因此驱动表的选择很是重要,驱动表的数据小能够显著下降扫描的行数。
那么为何通常状况下join的效率要高于left join不少?不少人说不明白缘由,只人云亦云,我今天下午感悟出来了一点。通常状况下参与联合查询的两张表都会一大一小,若是是join,在没有其余过滤条件的状况下MySQL会选择小表做为驱动表,可是left join通常用做大表去join小表,而left join自己的特性决定了MySQL会用大表去作驱动表,这样下来效率就差了很多,若是我把上面那个sql改为
select c.* from hotel_info_original c
join hotel_info_collection h
on c.hotel_type=h.hotel_type and c.hotel_id =h.hotel_id
查询计划以下:
很明显,MySQL选择了小表做为驱动表,再配合(hotel_id,hotel_type)上的索引瞬间下降了好多个数量级。。。。。
另外,我今天还明白了一个关于left join 的通用法则,即:若是where条件中含有右表的非空条件(除开is null),则left join语句等同于join语句,可直接改写成join语句。
后记:
随着查看MySQL reference manual对这个问题进行了更进一步的了解。MySQL在执行join时会把join分为system/const/eq_ref/ref/range/index/ALl等好几类,链接的效率从前日后
依次递减,对于个人第一个sql,链接类型是index,因此几乎是全表扫描的效果。可是我很奇怪我在(hotel_id,hotel_type)两列上声明了unique key,根据官方文档链接类型应该是eq_ref才对,
这个问题一直困扰了我两天,在google和stackoverflow上都没有找到可以解释这个问题的文章,莫非我这个问题无解了?抱着解决这个问题的决心今天又翻看了一遍MySQL官方文档
关于优化查询的部分,看到了这样一句:这里的一个问题是MySQL能更高效地在声明具备相同类型和尺寸的列上使用索引。我感受我找到了问题所在,因而我将original和 collection表的(hotel_type,hotel_id)的encoding和collation(决定字符比较的规则)所有改为统一的utf8_general_ci,而后再次运行第一条sql的查询计划,获得以下结果:
链接类型已经由index优化到了ref,若是将hotel_type申明为not null能够优化到eq_ref,不过这里影响不大了,优化后这条sql能在0.01ms内运行完。
那么如何优化left join:
一、条件中尽可能可以过滤一些行将驱动表变得小一点,用小表去驱动大表
二、右表的条件列必定要加上索引(主键、惟一索引、前缀索引等),最好可以使type达到range及以上(ref,eq_ref,const,system)
三、无视以上两点,通常不要用left join~~! 算法