本文大部分理论和问题都从maxcomputer中得出,不同的计算引擎底层结构不同最后结果可能稍微不同,这一点需要注意,本文应该可以让你不再苦恼各种join,或者更加苦恼join。
老生常谈的大小表问题
和传统sql语句类比,maxcomputer下的左右连接很奇怪,在官方文档中写着,碰到大小表问题,需要将大表放左边,小表放右边,和我们所理解的大小表问题的处理方式正好相反。
为了验证这一原因,我用一份两千万数据的大表和一份三万条数据的小表进行测试,为了避免数据泄露,本次测试包括下面全部测试样例都使用了CTE表达式。
正常的LEFTJOIN
首先说结果大表leftjoin小表,任务耗时3537ms,小表leftjoin大表,任务耗时4412ms。
从自带的logview查看二者的fuxijob,可以看出大表在右的时候,maxcomputer会采用hashjoin的方式直接将小表的数据和大表的数据关联;而当小表在右的时候,maxcomputer则会比正常多一步,既将两个数据流进行mergejoin,前者是将小表数据写入内存中进行join,后者则是排序join,耗时原因也就找到了。
排序后的LEFTJOIN
可实际业务中却也会出现左侧表为小表的情况,比如我们拿订单主表来关联订单子表的数据,如果提前以关联键排序,理论上来说,mergejoin会比hashjoin快一些,实际情况呢?
大表leftjoin小表,任务耗时6656ms,小表leftjoin大表,任务耗时4878ms,这真是可喜可贺,既然不能提升自己,那就打压一下对手,这里就不得不吐槽一下,一般大表在前,小表在后,是用大表卡小表的数据,用mapjoin中的join来指定大小表即可,为何要把其放在默认的逻辑上,反而是小表在前,会出现不用小表卡大表数据的情况。
这里也可能是本人接触的业务导致的偏见,继续聊join。
指定的MAPJOIN
maxomputer支持的mapjoin为左右连接和内连接,不支持全连接,但即使用mapjoin,leftouterjoin的时候左侧表也必须是大表,rightouterjoin相反,右侧为大表,innerjoin则可以显示指定何为小表,所以这里仅仅测试innerjoin的区别。
大表leftouterjoin小表,任务耗时7315ms,小表leftouterjoin大表,任务耗时8803ms;二者都是进行hashjoin,任务也只有两个M一个R,前者速度慢的原因,可能是我先查的后者,前者部分数据是从高速缓存读取的原因。
作为参考项,这里不使用mapjoin,单独join出了一份数据。大表leftouterjoin小表,任务耗时1499ms,小表leftouterjoin大表,任务耗时906ms;不用大惊小怪,因为maxcomputer有个类似oracle高速缓存的东西,也可以保存几次语义树解析相同的数据,需要的时候,直接拿出来。
为了进一步测试,过了一天后,我重新测试了非mapjoin的情况下,join的情况,大表在右时耗时7211ms,而小表在右则为8482ms.
到这里,大小表问题就告一段落,得出的结论也很简单,那就是大表在前,小表在后,如果数据需要出排序的数据,则可以提前排序,来让大小表的关系反过来,mapjoin的使用可有可无,基本上不是为了能够在关联的时候使用不等值表达式或者or逻辑,则可以舍弃mapjoin,其性能并不会因为使用了复杂的语法而多突出。
花里胡哨的JOIN
除了左右连接,内连接和隐式连接外,maxcomputer还提供了好几种花里胡哨的join,虽然可用性很低,但到了该使用的时候,也能带来不少的感官提升。
自然连接
作为自然连接,natutal join并不算稀奇,但他在odps sql的出现,不仅可以让开发少写几行代码,还可以让后来者在查看代码时,感到无地自容,这人的代码好牛逼,虽然只是sql,但我不知道他的关联是如何实现的。
其本质就是可以不去写on语句,只需要写natural join,执行时会自动根据左右表相同名称的字段进行inner join的操作。
半连接
半连接semi join,又一种朴实无华的join,其可以写为left semi join,可以返回左表满足右表指定条件的数据,和in相同;也可以写left anti join,可以返回左表不满足右表指定关联条件的数据,和not in相同。
IN和JOIN的效率
为了验证in和semi join在maxcomputer的执行效率,我用一张分区表进行了测试,大表数据大约1个g的数据量,小表数据为其一个分区有600多m的数据量,为了控制变量,在with阶段,我只选用了用户字段作为大小表数据,第一次semi join耗时3608ms,会有两个map任务,然后执行一个merge join任务,最后是有个reduce任务。
但在执行in的时候,语句进入resultcache直接得出结果,我在这里理解为二者经过优化后,实际执行的效率是相同的。
为了进一步测试,我用其他日期分区的再次进行in查询,发现执行为3517ms,查看job,发现in也是两个map任务,一个mergejoin任务,一个reduce任务,进一步验证了我的猜想。
这一理论也可以相应的延伸到其他的mapreduce为底层的sql语句,关系型数据库in比join慢是因为会创建临时表等操作,mapreduce并不会。
当然,我这里推荐使用的依旧还是semijoin,虽然可读性会差一些,只不过后续调整会方便很多,而且写的字数也比写in更少,效率就是王道!
Distributed MapJoin
分布式mapjoin(Distributed MapJoin),其为mapjoin的升级版,适合用作小表join大表的场景,不过核心都是减少大表侧的shuffle和排序操作。
只不过这个大小表有额外的定义,就是小表都得是在1g以上100g以下的数据,大表则是在10t以上的数据,这着实有点离大谱,目前本人还没有接触过这么大量级的表。
可以通过设置shard_count控制小表数据分片数,让小表数据分片(推荐一个分片量级为200m到500m)到各个计算节点处理;还可以通过replica_count控制小表数据的副本数,减少访问压力和避免节点失效后的整个任务失败,毕竟这一个任务跑起来消耗的资源可不少。
distmapjoin的真正使用场景,目前我无法想出来,但估计会发生的dws层,业务有一张大的维表和一张多数据源组层的dwd宽表。
SKEWJOIN HINT
sql join的时候有可能出现数据分布不均匀导致的长尾问题,skew join可以获取表的热点key,然后分别计算热点数据和非热点数据,加快执行效率。
显式指定和不指定的区别就在于,效率会更高,也就是方法三比方法一更快,方法二则居中。
当然,如果没有热点问题建议不要使用,它会让你的执行效率从3000ms变成10000ms,因为它的本质就是将热点数据和非热点数据打散,然后再unionall在一起。
动态过滤器
动态过滤器(dynamic filter)说是join的一种方式,反而不如说是一种join优化的思路,shuffle操作在海量数据下消耗的资源和时间都很多。
能够在join之前,将数据提前过滤掉的操作就被称为动态过滤器,官方文档中给出的思路是将小表侧的最小值和最大值得出,发送到大表侧,然后过滤掉大表侧的数据,规定中左侧为小表,而右侧为大表,但maxomputer定义join的时候,却是大表在左侧小表在右侧。
开启动态过滤器测试,发现他确实指定的small_table为小表,然后拿小表的最大值最小值去卡大表的数据,当大小表位置更换后则会报错。
官方文档也确实有如下内容(生产者为小表,消费者为大表,指大表消费小表生产的最大值最小值):
对于不同类型的JOIN语义,JOIN对象可担任的角色不同:
- A JOIN B:A或B都能作为生产者、消费者。
- A LEFT JOIN B:A只能作为生产者,B只能作为消费者。
- A RIGHT JOIN B:A只能作为消费者,B只能作为生产者。
- A FULL OUTER JOIN B:无法使用动态过滤器功能。
不懂就问,有大佬可以解释一下,为什么动态过滤器打开后,大小表的关系就反过来了?还是我测试的时候出现什么问题?
上面的思路和join方式套用在其他以mapreduce为基底的join上也是通用的.,当然就像我最后卡了bug一样,一段sql并不仅仅是写完就结束,他应该是优美的,尽自己可能优化过的。