MapReduce中的Join

  • 1、SQL中的关联
  • sql中多表关联方式
  • join:实现多张表之间列与列的关联
  • union:实现多张表之间行与行的关联
  • 子查询:查询语句的嵌套
  • 笛卡尔积:两张数据表,不指定关联条件
  • 2、SQL中的join
  • 3、MapReduce中的Join
  • 需求
  • 分析
  • step1:结果长什么样?
  • step2:有没有分组或者排序
  • step3:Map输出的Value的是什么?
  • step4:验证
  • 4、ReduceJoin的实现
  • 5、Map Jon的实现


1、SQL中的关联

  • 多表关联:实现两张表之间数据的联系
  • 订单表:订单id、商品id、订单价格
  • 商品表:商品id、商品名称
  • 需求:得到每个订单的订单信息以及对应的商品的名称
  • 前提:两张表需要有联系商品id

sql中多表关联方式

join:实现多张表之间列与列的关联

一般用于如果结果中出现了多张表的表,就用join实现

  • 订单表:订单id、商品id、订单价格
  • 商品表:商品id、商品名称
  • 将两张表的列进行合并
  • 订单id、商品id、订单价格、商品id、商品名称
select * 
from 订单表 a 
join 商品表 b on a.商品id = b.商品id;

union:实现多张表之间行与行的关联

  • table1:小学的学生信息
  • 学生id、学生姓名、学生入学年龄……
  • table2:中学的学生信息
  • 学生id、学生姓名、学生入学年龄……
  • 需求:实现将两张表中的数据进行合并
insert into table3 
select * from table1 
union 
select * from table2;

子查询:查询语句的嵌套

查询结果只有一张表的列,但是查询的条件依赖于另外一张表

  • 订单表:订单id、商品id、订单价格
  • 商品表:商品id、商品名称
  • 需求:我想查询商品名称叫做牙刷的所有订单信息
select * 
from 订单表 
where 商品id =(select 商品id 
				from 商品表 
				where 商品名称 = 牙刷)

笛卡尔积:两张数据表,不指定关联条件

select * from a,b;
select * from a join b;

由于没有指定条件,a的每一条关联了b的每一条笛卡尔积的结果有多少条:a的条数*b的条数

  • 订单表
1001,20150710,p0001,2
1002,20150710,p0002,3
1002,20150710,p0003,3
1003,20150710,p0003,3
  • 商品表
p0001,直升机,1000,2000
p0002,坦克,1000,3000
p0003,火箭,10000,2000
p0004,装甲车,10000,2000
  • 笛卡尔积:4*4= 16条数据
  • 大数据中一般严禁产生笛卡尔积
  • 表:1亿
  • 表:2亿
  • 笛卡尔积会产生1亿*2亿!!!

2、SQL中的join

  • inner join:内连接:两张表都有结果才有
  • outer join:外连接
  • left outer join:左表有,结果就有
  • right outer join :右表有,结果就有
  • full join:全连接,任何一张表有,结果就有
  • Mysql中不支持
  • 如果对方没有,以null来补齐
--订单表
1001,20150710,p0001,2
1002,20150710,p0002,3
1002,20150710,p0003,3
1003,20150710,p0005,3
--商品表
p0001,直升机,1000,2000
p0002,坦克,1000,3000
p0003,火箭,10000,2000
p0004,装甲车,10000,2000
--full join:5条
1001,20150710,p0001,2       p0001,直升机,1000,2000
1002,20150710,p0002,3       p0002,坦克,1000,3000
1002,20150710,p0003,3       p0003,火箭,10000,2000
1003,20150710,p0005,3       null,null,null,null
null,null,null,null       p0004,装甲车,10000,2000

3、MapReduce中的Join

基于文件实现关联

需求

  • 订单文件:orders.txt
1001,20150710,p0001,2
1002,20150710,p0002,3
1002,20150710,p0003,3
  • 商品文件:product.txt
p0001,直升机,1000,2000
p0002,坦克,1000,3000
p0003,火箭,10000,2000

基于商品和订单数据实现关联,得到每个订单的信息以及商品的名称

分析

step1:结果长什么样?

1001,20150710,p0001,2   直升机
1002,20150710,p0002,3   坦克
1002,20150710,p0003,3   火箭

reduceJoin:

p0001   直升机	 1001    20150710    2   
p0002   坦克		 1002    20150710    3    
p0003   火箭 	 1002    20150710    3

step2:有没有分组或者排序

没有排序

  • 有没有分组呢?
  • 总共 输入6条数据
  • 是按照订单分组吗?
  • 不是,因为商品信息中是没有订单的
  • 要分组的字段一定是两份数据都有的字段:就是关联字段
  • 按照商品id进行分组
  • map输出的Key就是商品id

step3:Map输出的Value的是什么?

  • 总共 输入6条数据,进入Map就是6个KV
  • 需要对数据做判断
  • 如果这条数据是订单数据
  • value就是除了商品id以外的订单的信息
  • 如果这条数据是商品数据
  • value就是商品名称

step4:验证

  • Input
1001,20150710,p0001,2
1002,20150710,p0002,3
1002,20150710,p0003,3
p0001,直升机,1000,2000
p0002,坦克,1000,3000
p0003,火箭,10000,2000
  • Map
key             value
p0001       1001,20150710,2
p0002       1002,20150710,3
p0003       1002,20150710,3
p0001       直升机
p0002       坦克
p0003       火箭
  • Shuffle
    分组:按照商品id进行分组
key             Itrable:value
p0001       1001,20150710,2 直升机
p0002       1002,20150710,3  坦克
p0003       1002,20150710,3  火箭
  • reduce
  • 迭代将订单信息和商品名称拼接输出即可

4、ReduceJoin的实现

package bigdata.hanjiaxiaozhi.cn.mr.join;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import java.io.IOException;

/**
 * @ClassName MRDriver
 * @Description TODO 实现ReduceJoin
 * @Date 2020/5/30 10:34
 * @Create By     hanjiaxiaozhi
 */
public class ReduceJoinMr extends Configured implements Tool {

    /**
     * 用于将Job的代码封装
     * @param args
     * @return
     * @throws Exception
     */
    @Override
    public int run(String[] args) throws Exception {
        //todo:1-构建一个Job
        Job job = Job.getInstance(this.getConf(),"model");//构建Job对象,调用父类的getconf获取属性的配置
        job.setJarByClass(ReduceJoinMr.class);//指定可以运行的类型
        //todo:2-配置这个Job
        //input
//        job.setInputFormatClass(TextInputFormat.class);//设置输入的类的类型,默认就是TextInputFormat
        Path inputPath1 = new Path("datas/join/orders.txt");//读取订单数据
        Path inputPath2 = new Path("datas/join/product.txt");//读取商品的数据
        //设置的路径可以给目录,也可以给定文件,如果给定目录,会将目录中所有文件作为输入,但是目录中不能包含子目录
        TextInputFormat.setInputPaths(job,inputPath1,inputPath2);//为当前job设置输入的路径

        //map
        job.setMapperClass(MRMapper.class);//设置Mapper的类,需要调用对应的map方法
        job.setMapOutputKeyClass(Text.class);//设置Mapper输出的key类型
        job.setMapOutputValueClass(Text.class);//设置Mapper输出的value类型

        //shuffle
//        job.setPartitionerClass(HashPartitioner.class);//自定义分区
//        job.setGroupingComparatorClass(null);//自定义分组的方式
//        job.setSortComparatorClass(null);//自定义排序的方式

        //reduce
        job.setReducerClass(MRReducer.class);//设置Reduce的类,需要调用对应的reduce方法
        job.setOutputKeyClass(Text.class);//设置Reduce输出的Key类型
        job.setOutputValueClass(Text.class);//设置Reduce输出的Value类型
        job.setNumReduceTasks(1);//设置ReduceTask的个数,默认为1

        //output:输出目录默认不能提前存在
//        job.setOutputFormatClass(TextOutputFormat.class);//设置输出的类,默认我诶TextOutputFormat
        Path outputPath = new Path("datas/output/join/reduceJoin");//用程序的第三个参数作为输出
        //解决输出目录提前存在,不能运行的问题,提前将目前删掉
        //构建一个HDFS的文件系统
        FileSystem hdfs = FileSystem.get(this.getConf());
        //判断输出目录是否存在,如果存在就删除
        if(hdfs.exists(outputPath)){
            hdfs.delete(outputPath,true);
        }
        TextOutputFormat.setOutputPath(job,outputPath);//为当前Job设置输出的路径

        //todo:3-提交运行Job
        return job.waitForCompletion(true) ? 0:-1;
    }

    /**
     * 程序的入口,调用run方法
     * @param args
     */
    public static void main(String[] args) throws Exception {
        //构建一个Configuration对象,用于管理这个程序所有配置,工作会定义很多自己的配置
        Configuration conf = new Configuration();
        //t通过Toolruner的run方法调用当前类的run方法
        int status = ToolRunner.run(conf, new ReduceJoinMr(), args);
        //退出程序
        System.exit(status);
    }


    /**
     * @ClassName MRMapper
     * @Description TODO 这是MapReduce模板的Map类
     *      输入的KV类型:由inputformat决定,默认是TextInputFormat
     *      输出的KV类型:由map方法中谁作为key,谁作为Value决定
     */
    public static class MRMapper extends Mapper<LongWritable, Text, Text,Text> {

        Text outputKey= new Text();
        Text outputValue = new Text();


        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //两个文件的每一行数据,就是Value
            // 要判断这条数据来自哪个文件
            FileSplit fileSplit = (FileSplit) context.getInputSplit();//获取这条数据对应的分片
            String name = fileSplit.getPath().getName();//获取这条数据对应的文件的名称
            if("orders.txt".equals(name)){
                //如果这是订单的数据:1001,20150710,p0001,2
                String[] split1 = value.toString().split(",");
                //用商品id作为Key
                this.outputKey.set(split1[2]);
                //其他信息作为Value
                this.outputValue.set(split1[0]+"\t"+split1[1]+"\t"+split1[3]);
                //输出
                context.write(this.outputKey,this.outputValue);
            }else{
                //这是商品数据:p0001,直升机,1000,2000
                String[] split2 = value.toString().split(",");
                //用商品id作为key
                this.outputKey.set(split2[0]);
                //用商品名称作为Value
                this.outputValue.set(split2[1]);
                //输出
                context.write(this.outputKey,this.outputValue);
            }
        }
    }



    /**
     * @ClassName MRReducer
     * @Description TODO MapReduce模板的Reducer的类
     *      输入的KV类型:由Map的输出决定,保持一致
     *      输出的KV类型:由reduce方法中谁作为key,谁作为Value决定
     */
    public static class MRReducer extends Reducer<Text,Text,Text,Text> {
        @Override
        protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
            //传进来的是每个商品id对应的订单信息和商品名称
            StringBuilder stringBuilder = new StringBuilder();
            for (Text value : values) {
                //将订单信息和商品的名称进行拼接
                stringBuilder.append(value.toString()+"\t");
            }
            //输出
            context.write(key,new Text(stringBuilder.toString()));

        }
    }


}


  • Reduce Join
  • 根据两份数据的关联条件作为分组的条件
  • 在Shuffle中进行了分组
  • 相同id的数据进入了同一组,在Reduce中实现关联的实现
  • 应用场景:大的数据join大的数据
  • 必须经过shuffle的分组,会带来不必要的麻烦,做排序、会产生文件,非常慢

5、Map Jon的实现

  • 应用场景:适合于小数据量 join 大数据量
  • 设计思想:将小数据量的文件放到分布式内存中【每一台机器都有】,谁要谁取
  • 优点:不经过shuffle过程,不需要将三台机器之间的数据进行交换,然后统一的分组
  • 实现
package bigdata.hanjiaxiaozhi.cn.mr.join;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName MRDriver
 * @Description TODO 这是MapReduce程序的Driver类的模板
 * @Date 2020/5/30 10:34
 * @Create By     hanjiaxiaozhi
 */
public class MapJoinMr extends Configured implements Tool {

    /**
     * 用于将Job的代码封装
     * @param args
     * @return
     * @throws Exception
     */
    @Override
    public int run(String[] args) throws Exception {
        //todo:1-构建一个Job
        Job job = Job.getInstance(this.getConf(),"model");//构建Job对象,调用父类的getconf获取属性的配置
        job.setJarByClass(MapJoinMr.class);//指定可以运行的类型
        //todo:2-配置这个Job
        //input
//        job.setInputFormatClass(TextInputFormat.class);//设置输入的类的类型,默认就是TextInputFormat
        Path inputPath = new Path("datas/join/orders.txt");//将大的数据进行读取
        //将订单作为输入
        TextInputFormat.setInputPaths(job,inputPath);//为当前job设置输入的路径
        //将商品表放入分布式缓存中
        job.addCacheFile(new Path("datas/join/product.txt").toUri());

        //map
        job.setMapperClass(MRMapper.class);//设置Mapper的类,需要调用对应的map方法
        job.setMapOutputKeyClass(Text.class);//设置Mapper输出的key类型
        job.setMapOutputValueClass(Text.class);//设置Mapper输出的value类型

        //shuffle
//        job.setPartitionerClass(HashPartitioner.class);//自定义分区
//        job.setGroupingComparatorClass(null);//自定义分组的方式
//        job.setSortComparatorClass(null);//自定义排序的方式

        //reduce
//        job.setReducerClass(MRReducer.class);//设置Reduce的类,需要调用对应的reduce方法
//        job.setOutputKeyClass(NullWritable.class);//设置Reduce输出的Key类型
//        job.setOutputValueClass(NullWritable.class);//设置Reduce输出的Value类型
        job.setNumReduceTasks(0);//设置ReduceTask的个数,默认为1

        //output:输出目录默认不能提前存在
//        job.setOutputFormatClass(TextOutputFormat.class);//设置输出的类,默认我诶TextOutputFormat
        Path outputPath = new Path("datas/output/join/mapJoin");//用程序的第三个参数作为输出
        //解决输出目录提前存在,不能运行的问题,提前将目前删掉
        //构建一个HDFS的文件系统
        FileSystem hdfs = FileSystem.get(this.getConf());
        //判断输出目录是否存在,如果存在就删除
        if(hdfs.exists(outputPath)){
            hdfs.delete(outputPath,true);
        }
        TextOutputFormat.setOutputPath(job,outputPath);//为当前Job设置输出的路径

        //todo:3-提交运行Job
        return job.waitForCompletion(true) ? 0:-1;
    }

    /**
     * 程序的入口,调用run方法
     * @param args
     */
    public static void main(String[] args) throws Exception {
        //构建一个Configuration对象,用于管理这个程序所有配置,工作会定义很多自己的配置
        Configuration conf = new Configuration();
        //t通过Toolruner的run方法调用当前类的run方法
        int status = ToolRunner.run(conf, new MapJoinMr(), args);
        //退出程序
        System.exit(status);
    }


    /**
     * @ClassName MRMapper
     * @Description TODO 这是MapReduce模板的Map类
     *      输入的KV类型:由inputformat决定,默认是TextInputFormat
     *      输出的KV类型:由map方法中谁作为key,谁作为Value决定
     */
    public static class MRMapper extends Mapper<LongWritable, Text, Text,Text> {
        //获取分布式缓存中的数据,存入Map集合,Key是商品id,Value是商品名称
        Map<String,String> map = new HashMap<>();


        /**
         * Map类中有三个方法
         *      setup:在调用map之前会调用一次,类似于初始化的方法
         *      map:实现Map处理逻辑的方法
         *      cleanup:Map结束以后会调用的方法,相当于close方法
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            //将缓存中的数据读取出来,封装到Map集合中
            URI[] cacheFiles = context.getCacheFiles();
            //打开这个缓存的文件
            BufferedReader bufferedReader = new BufferedReader(new FileReader(cacheFiles[0].getPath()));
            //将每一行的内容,封装到Map集合中
            String line = null;
            while(StringUtils.isNotBlank(line = bufferedReader.readLine())){
                //分割将商品id和商品名称放入Map集合
                String pid = line.split(",")[0];
                String pname = line.split(",")[1];
                map.put(pid,pname);
            }
        }

        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
           //只有订单的数据,取出订单中的商品id
            String pid = value.toString().split(",")[2];
            //在Map集合中找到这个商品id对应的商品名称
            String name = map.get(pid);
            //输出订单信息和商品名称
            context.write(value,new Text(name));
        }
    }

}