Spark的join實現的3種方式(與Hive中的join對比)

@Author  : Spinach | GHB
@Link    : http://blog.csdn.net/bocai8058

1 Spark的join與Hive的join對比

1.1 數據準備

準備兩張Hive表,分別是orders(訂單表)和drivers(司機表),通過driver_id字段進行關聯。數據如下:

// orders表有兩個字段,訂單id:order_id和司機id:driver_id。司機id將作爲連接鍵。

hive (test)> select * from orders;
OK
orders.order_id orders.driver_id
1000    5000
1001    5001
1002    5002
Time taken: 0.387 seconds, Fetched: 3 row(s)
// drivers表由兩個字段,司機id:driver_id和車輛id:car_id。司機id將作爲連接鍵。

hive (test)> select * from drivers;
OK
drivers.driver_id       drivers.car_id
5000    100
5003    103
Time taken: 0.036 seconds, Fetched: 2 row(s)

1.2 Hive的join、left outer join、right outer join

join:自然連接,輸出連接鍵匹配的記錄。

可以看到,通過driver_id匹配的數據只有一條。

hive (test)> select * from orders t1 join drivers t2 on (t1.driver_id = t2.driver_id) ;
OK
t1.order_id     t1.driver_id    t2.driver_id    t2.car_id
1000    5000    5000    100
Time taken: 36.079 seconds, Fetched: 1 row(s)

left outer join:左外鏈接,輸出連接鍵匹配的記錄,左側的表無論匹配與否都輸出。

可以看到,通過driver_id匹配的數據只有一條,不過所有orders表中的記錄都被輸出了,drivers中未能匹配的字段被置爲空。

hive (test)> select * from orders t1 left outer join drivers t2 on (t1.driver_id = t2.driver_id) ;
OK
t1.order_id     t1.driver_id    t2.driver_id    t2.car_id
   5000    5000    100
   5001    NULL    NULL
   5002    NULL    NULL
Time taken: 36.063 seconds, Fetched: 3 row(s)

right outer join:右外連接,輸出連接鍵匹配的記錄,右側的表無論匹配與否都輸出。

可以看到,通過driver_id匹配的數據只有一條,不過所有drivers表中的記錄都被輸出了,orders中未能匹配的字段被置爲空。

hive (test)> select * from orders t1 right outer join drivers t2 on (t1.driver_id = t2.driver_id) ;
OK
t1.order_id     t1.driver_id    t2.driver_id    t2.car_id
1000    5000    5000    100
NULL    NULL    5003    103
Time taken: 30.089 seconds, Fetched: 2 row(s)

1.3 Spark的join、leftOuterJoin、rightOuterJoin

spark實現join的方式也是通過RDD的算子,spark同樣提供了三個算子join,leftOuterJoin,rightOuterJoin。

在下面給出的例子中,我們通過spark-hive讀取了Hive中orders表和drivers

  • 首先需要先將DataFrame轉化成了JavaRDD。
  • 不過,JavaRDD其實是沒有join算子的,下面還需要通過mapToPair算子將JavaRDD轉換成JavaPairRDD,這樣就可以使用Join了。

下面例子中給出了三種join操作的實現方式,在join之後,通過collect()函數把數據拉到Driver端本地,並通過標準輸出打印。
需要指出的是

  1. join算子(join,leftOuterJoin,rightOuterJoin)只能通過PairRDD使用;
  2. join算子操作的Tuple2<Object1, Object2>類型中,Object1是連接鍵,我只試過Integer和String,Object2比較靈活,甚至可以是整個Row。

這裏我們使用driver_id作爲連接鍵。 所以在輸出Tuple2的時候,我們將driver_id放在了前面。

/*
*   spark-submit --queue=root.zhiliangbu_prod_datamonitor spark-join-1.0-SNAPSHOT-jar-with-dependencies.jar
* */
public class Join implements Serializable {

    private transient JavaSparkContext javaSparkContext;
    private transient HiveContext hiveContext;

    /*
    *   初始化Load
    *   創建sparkContext, sqlContext, hiveContext
    * */
    public Join() {
        initSparckContext();
        initHiveContext();
    }

    /*
    *   創建sparkContext
    * */
    private void initSparckContext() {
        String warehouseLocation = System.getProperty("user.dir");
        SparkConf sparkConf = new SparkConf()
                .setAppName("spark-join")
                .set("spark.sql.warehouse.dir", warehouseLocation)
                .setMaster("yarn-client");
        javaSparkContext = new JavaSparkContext(sparkConf);
    }

    /*
    *   創建hiveContext
    *   用於讀取Hive中的數據
    * */
    private void initHiveContext() {
        hiveContext = new HiveContext(javaSparkContext);
    }


    public void join() {
        /*
        *   生成rdd1
        * */
        String query1 = "select * from gulfstream_test.orders";
        DataFrame rows1 = hiveContext.sql(query1).select("order_id", "driver_id");
        JavaPairRDD<String, String> rdd1 = rows1.toJavaRDD().mapToPair(new PairFunction<Row, String, String>() {
            @Override
            public Tuple2<String, String> call(Row row) throws Exception {
                String orderId = (String)row.get(0);
                String driverId = (String)row.get(1);
                return new Tuple2<String, String>(driverId, orderId);
            }
        });
        /*
        *   生成rdd2
        * */
        String query2 = "select * from gulfstream_test.drivers";
        DataFrame rows2 = hiveContext.sql(query2).select("driver_id", "car_id");
        JavaPairRDD<String, String> rdd2 = rows2.toJavaRDD().mapToPair(new PairFunction<Row, String, String>() {
            @Override
            public Tuple2<String, String> call(Row row) throws Exception {
                String driverId = (String)row.get(0);
                String carId = (String)row.get(1);
                return new Tuple2<String, String>(driverId, carId);
            }
        });
        /*
        *   join
        * */
        System.out.println(" ****************** join *******************");
        JavaPairRDD<String, Tuple2<String, String>> joinRdd = rdd1.join(rdd2);
        Iterator<Tuple2<String, Tuple2<String, String>>> it1 = joinRdd.collect().iterator();
        while (it1.hasNext()) {
            Tuple2<String, Tuple2<String, String>> item = it1.next();
            System.out.println("driver_id:" + item._1 + ", order_id:" + item._2._1 + ", car_id:" + item._2._2 );
        }

        /*
        *   leftOuterJoin
        * */
        System.out.println(" ****************** leftOuterJoin *******************");
        JavaPairRDD<String, Tuple2<String, Optional<String>>> leftOuterJoinRdd = rdd1.leftOuterJoin(rdd2);
        Iterator<Tuple2<String, Tuple2<String, Optional<String>>>> it2 = leftOuterJoinRdd.collect().iterator();
        while (it2.hasNext()) {
            Tuple2<String, Tuple2<String, Optional<String>>> item = it2.next();
            System.out.println("driver_id:" + item._1 + ", order_id:" + item._2._1 + ", car_id:" + item._2._2 );
        }

        /*
        *   rightOuterJoin
        * */
        System.out.println(" ****************** rightOuterJoin *******************");
        JavaPairRDD<String, Tuple2<Optional<String>, String>> rightOuterJoinRdd = rdd1.rightOuterJoin(rdd2);
        Iterator<Tuple2<String, Tuple2<Optional<String>, String>>> it3 = rightOuterJoinRdd.collect().iterator();
        while (it3.hasNext()) {
            Tuple2<String, Tuple2<Optional<String>, String>> item = it3.next();
            System.out.println("driver_id:" + item._1 + ", order_id:" + item._2._1 + ", car_id:" + item._2._2 );
        }
    }

    public static void main(String[] args) {
        Join sj = new Join();
        sj.join();
    }

}

執行結果

其中Optional.absent()表示的就是null,可以看到和HSQL是一致的。

Application ID is application_1508228032068_2746260, trackingURL: http://10.93.21.21:4040
 ****************** join *******************
driver_id:5000, order_id:1000, car_id:100                                       
 ****************** leftOuterJoin *******************
driver_id:5001, order_id:1001, car_id:Optional.absent()
driver_id:5002, order_id:1002, car_id:Optional.absent()
driver_id:5000, order_id:1000, car_id:Optional.of(100)
 ****************** rightOuterJoin *******************
driver_id:5003, order_id:Optional.absent(), car_id:103
driver_id:5000, order_id:Optional.of(1000), car_id:100

由於數據量不大,我沒有從執行效率上進行考量。

根據經驗,一般在數據量較大的情況下,HSQL的執行效率會高一些,如果數據量較小,Spark會快。

2 SparkSQL的join實現

Join是SQL語句中的常用操作,良好的表結構能夠將數據分散在不同的表中,使其符合某種範式,減少表冗餘、更新容錯等。而建立表和表之間關係的最佳方式就是Join操作。

SparkSQL作爲大數據領域的SQL實現,自然也對Join操作做了不少優化,今天主要看一下在SparkSQL中對於Join,常見的3種實現。

2.1 Broadcast Join

大家知道,在數據庫的常見模型中(比如星型模型或者雪花模型),表一般分爲兩種:事實表和維度表。維度表一般指固定的、變動較少的表,例如聯繫人、物品種類等,一般數據有限。而事實表一般記錄流水,比如銷售清單等,通常隨着時間的增長不斷膨脹。

因爲Join操作是對兩個表中key值相同的記錄進行連接,在SparkSQL中,對兩個表做Join最直接的方式是先根據key分區,再在每個分區中把key值相同的記錄拿出來做連接操作。但這樣就不可避免地涉及到shuffle,而shuffle在Spark中是比較耗時的操作,我們應該儘可能的設計Spark應用使其避免大量的shuffle。

當維度表和事實表進行Join操作時,爲了避免shuffle,我們可以將大小有限的維度表的全部數據分發到每個節點上,供事實表使用。executor存儲維度表的全部數據,一定程度上犧牲了空間,換取shuffle操作大量的耗時,這在SparkSQL中稱作Broadcast Join,如下圖所示:

Table B是較小的表,黑色表示將其廣播到每個executor節點上,Table A的每個partition會通過block manager取到Table A的數據。根據每條記錄的Join Key取到Table B中相對應的記錄,根據Join Type進行操作。這個過程比較簡單,不做贅述。
Broadcast Join的條件有以下幾個:

  1. 被廣播的表需要小於spark.sql.autoBroadcastJoinThreshold所配置的值,默認是10M (或者加了broadcast join的hint)
  2. 基表不能被廣播,比如left outer join時,只能廣播右表
    看起來廣播是一個比較理想的方案,但它有沒有缺點呢?也很明顯。這個方案只能用於廣播較小的表,否則數據的冗餘傳輸就遠大於shuffle的開銷;另外,廣播時需要將被廣播的表現collect到driver端,當頻繁有廣播出現時,對driver的內存也是一個考驗。

2.2 Shuffle Hash Join

當一側的表比較小時,我們選擇將其廣播出去以避免shuffle,提高性能。但因爲被廣播的表首先被collect到driver段,然後被冗餘分發到每個executor上,所以當表比較大時,採用broadcast join會對driver端和executor端造成較大的壓力。

但由於Spark是一個分佈式的計算引擎,可以通過分區的形式將大批量的數據劃分成n份較小的數據集進行並行計算。這種思想應用到Join上便是Shuffle Hash Join了。利用key相同必然分區相同的這個原理,SparkSQL將較大表的join分而治之,先將表劃分成n個分區,再對兩個表中相對應分區的數據分別進行Hash Join,這樣即在一定程度上減少了driver廣播一側表的壓力,也減少了executor端取整張被廣播表的內存消耗。其原理如下圖:

Shuffle Hash Join分爲兩步:

  1. 對兩張表分別按照join keys進行重分區,即shuffle,目的是爲了讓有相同join keys值的記錄分到對應的分區中
  2. 對對應分區中的數據進行join,此處先將小表分區構造爲一張hash表,然後根據大表分區中記錄的join keys值拿出來進行匹配
    Shuffle Hash Join的條件有以下幾個:
  3. 分區的平均大小不超過spark.sql.autoBroadcastJoinThreshold所配置的值,默認是10M
  4. 基表不能被廣播,比如left outer join時,只能廣播右表
  5. 一側的表要明顯小於另外一側,小的一側將被廣播(明顯小於的定義爲3倍小,此處爲經驗值)
    我們可以看到,在一定大小的表中,SparkSQL從時空結合的角度來看,將兩個表進行重新分區,並且對小表中的分區進行hash化,從而完成join。在保持一定複雜度的基礎上,儘量減少driver和executor的內存壓力,提升了計算時的穩定性。

2.3 Sort Merge Join

上面介紹的兩種實現對於一定大小的表比較適用,但當兩個表都非常大時,顯然無論適用哪種都會對計算內存造成很大壓力。這是因爲join時兩者採取的都是hash join,是將一側的數據完全加載到內存中,使用hash code取join keys值相等的記錄進行連接。

當兩個表都非常大時,SparkSQL採用了一種全新的方案來對錶進行Join,即Sort Merge Join。這種實現方式不用將一側數據全部加載後再進星hash join,但需要在join前將數據排序,如下圖所示:

可以看到,首先將兩張表按照join keys進行了重新shuffle,保證join keys值相同的記錄會被分在相應的分區。分區後對每個分區內的數據進行排序,排序後再對相應的分區內的記錄進行連接,如下圖示:

看着很眼熟吧?也很簡單,因爲兩個序列都是有序的,從頭遍歷,碰到key相同的就輸出;如果不同,左邊小就繼續取左邊,反之取右邊。
可以看出,無論分區有多大,Sort Merge Join都不用把某一側的數據全部加載到內存中,而是即用即取即丟,從而大大提升了大數據量下sql join的穩定性。

本文介紹了SparkSQL中的3中Join實現,其實這也不是什麼新鮮玩意兒。傳統DB也有這也的玩法兒,SparkSQL只是將其做成分佈式的實現。
本文僅僅從大的理論方面介紹了這幾種實現,具體到每個join type是怎麼遍歷、沒有join keys時應該怎麼做、這些實現對join keys有什麼具體的需求,這些細節都沒有展現出來。感興趣的話,可以去翻翻源碼。

引用:https://www.cnblogs.com/kangoroo/p/7778962.html | https://blog.csdn.net/asongoficeandfire/article/details/53574034


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章