hive學習筆記之十一:UDTF

程式設計師欣宸發表於2021-07-11

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概覽

  1. 本文是《hive學習筆記》系列的第十一篇,截至目前,一進一出的UDF、多進一出的UDAF我們們都學習過了,最後還有一進多出的UDTF留在本篇了,這也是本篇的主要內容;
  2. 一進多出的UDTF,名為使用者自定義表生成函式(User-Defined Table-Generating Functions, UDTF);
  3. 前面的文章中,我們們曾經體驗過explode就是hive內建的UDTF:
hive> select explode(address) from t3;
OK
province	guangdong
city	shenzhen
province	jiangsu
city	nanjing
Time taken: 0.081 seconds, Fetched: 4 row(s)
  1. 本篇的UDTF一共有兩個例項:把一列拆成多列、把一列拆成多行(每行多列);
  2. 接下來開始實戰;

原始碼下載

  1. 如果您不想編碼,可以在GitHub下載所有原始碼,地址和連結資訊如下表所示:
名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  1. 這個git專案中有多個資料夾,本章的應用在hiveudf資料夾下,如下圖紅框所示:

在這裡插入圖片描述

準備工作

為了驗證UDTF的功能,我們們要先把表和資料都準備好:

  1. 新建名為t16的表:
create table t16(
person_name  string,
string_field string
)
row format delimited 
fields terminated by '|'
stored as textfile;
  1. 本地新建文字檔案016.txt,內容如下:
tom|1:province:guangdong
jerry|2:city:shenzhen
john|3
  1. 匯入資料:
load data 
local inpath '/home/hadoop/temp/202010/25/016.txt' 
overwrite into table t16;
  1. 資料準備完畢,開始編碼;

UDTF開發的關鍵點

  1. 需要繼承GenericUDTF類;
  2. 重寫initialize方法,該方法的入參只有一個,型別是StructObjectInspector,從這裡可以取得UDTF作用了幾個欄位,以及欄位型別;
  3. initialize的返回值是StructObjectInspector型別,UDTF生成的每個列的名稱和型別都設定到返回值中;
  4. 重寫process方法,該方法中是一進多出的邏輯程式碼,把每個列的資料準備好放在陣列中,執行一次forward方法,就是一行記錄;
  5. close方法不是必須的,如果業務邏輯執行完畢,可以將釋放資源的程式碼放在這裡執行;
  6. 接下來,就按照上述關鍵點開發UDTF;

一列拆成多列

  • 接下來要開發的UDTF,名為udf_wordsplitsinglerow,作用是將入參拆分成多個列;
  • 下圖紅框中是t16表的一條原始記錄的string_field欄位,會被udf_wordsplitsinglerow處理:

在這裡插入圖片描述

  • 上面紅框中的欄位被UDTF處理處理後,一列變成了三列,每一列的名稱如下圖黃框所示,每一列的值如紅框所示:

在這裡插入圖片描述

  • 以上就是我們們馬上就要開發的功能;
  • 開啟前文建立的hiveudf工程,新建WordSplitSingleRow.java:
package com.bolingcavalry.hiveudf.udtf;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.exec.UDFArgumentLengthException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTF;
import org.apache.hadoop.hive.serde2.objectinspector.*;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 把指定欄位拆成多列
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2020/11/5 14:43
 */
public class WordSplitSingleRow extends GenericUDTF {

    private PrimitiveObjectInspector stringOI = null;

    private final static String[] EMPTY_ARRAY = {"NULL", "NULL", "NULL"};

    /**
     * 一列拆成多列的邏輯在此
     * @param args
     * @throws HiveException
     */
    @Override
    public void process(Object[] args) throws HiveException {

        String input = stringOI.getPrimitiveJavaObject(args[0]).toString();

        // 無效字串
        if(StringUtils.isBlank(input)) {
            forward(EMPTY_ARRAY);
        } else {

            // 分割字串
            String[] array = input.split(":");

            // 如果字串陣列不合法,就返回原始字串和錯誤提示
            if(null==array || array.length<3) {
                String[] errRlt = new String[3];
                errRlt[0] = input;
                errRlt[1] = "can not split to valid array";
                errRlt[2] = "-";

                forward(errRlt);
            } else {
                forward(array);
            }
        }
    }

    /**
     * 釋放資源在此執行,本例沒有資源需要釋放
     * @throws HiveException
     */
    @Override
    public void close() throws HiveException {

    }

    @Override
    public StructObjectInspector initialize(StructObjectInspector argOIs) throws UDFArgumentException {

        List<? extends StructField> inputFields = argOIs.getAllStructFieldRefs();

        // 當前UDTF只處理一個引數,在此判斷傳入的是不是一個引數
        if (1 != inputFields.size()) {
            throw new UDFArgumentLengthException("ExplodeMap takes only one argument");
        }

        // 此UDTF只處理字串型別
        if(!Category.PRIMITIVE.equals(inputFields.get(0).getFieldObjectInspector().getCategory())) {
            throw new UDFArgumentException("ExplodeMap takes string as a parameter");
        }

        stringOI = (PrimitiveObjectInspector)inputFields.get(0).getFieldObjectInspector();

        //列名集合
        ArrayList<String> fieldNames = new ArrayList<String>();

        //列對應的value值
        ArrayList<ObjectInspector> fieldOIs = new ArrayList<ObjectInspector>();

        // 第一列的列名
        fieldNames.add("id");
        // 第一列的inspector型別為string型
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        // 第二列的列名
        fieldNames.add("key");
        // 第二列的inspector型別為string型
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        // 第三列的列名
        fieldNames.add("value");
        // 第三列的inspector型別為string型
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        return ObjectInspectorFactory.getStandardStructObjectInspector(fieldNames, fieldOIs);
    }
}

  • 上述程式碼中的重點是process方法,取得入參後用冒號分割字串,得到陣列,再呼叫forward方法,就生成了一行記錄,該記錄有三列;

驗證UDTF

接下來將WordSplitSingleRow.java部署成臨時函式並驗證;

  1. 編碼完成後,在pom.xml所在目錄執行命令mvn clean package -U
  2. 在target目錄得到檔案hiveudf-1.0-SNAPSHOT.jar
  3. 將jar下載到hive伺服器,我這裡放在此目錄:/home/hadoop/udf/
  4. 在hive會話模式執行以下命令新增本地jar:
add jar /home/hadoop/udf/hiveudf-1.0-SNAPSHOT.jar;
  1. 部署臨時函式:
create temporary function udf_wordsplitsinglerow as 'com.bolingcavalry.hiveudf.udtf.WordSplitSingleRow';
  1. 執行以下SQL驗證:
select udf_wordsplitsinglerow(string_field) from t16;
  1. 結果如下,可見每一行記錄的string_field欄位都被分割成了id、key、value三個欄位:
hive> select udf_wordsplitsinglerow(string_field) from t16;
OK
id	key	value
1	province	guangdong
2	city	shenzhen
3	can not split to valid array	-
Time taken: 0.066 seconds, Fetched: 3 row(s)

關鍵點要注意

  • 值得注意的是,UDTF不能和其他欄位同時出現在select語句中,例如以下的SQL會執行失敗:
select person_name,udf_wordsplitsinglerow(string_field) from t16;
  • 錯誤資訊如下:
hive> select person_name,udf_wordsplitsinglerow(string_field) from t16;
FAILED: SemanticException [Error 10081]: UDTF's are not supported outside the SELECT clause, nor nested in expressions
  • 如果希望得到UDTF和其他欄位的結果,可以使用LATERAL VIEW語法,完整SQL如下:
select t.person_name, udtf_id, udtf_key, udtf_value
from (
    select person_name, string_field 
    from  t16
) t LATERAL VIEW udf_wordsplitsinglerow(t.string_field) v as  udtf_id, udtf_key, udtf_value;
  • 查詢結果如下,可見指定欄位和UDTF都能顯示:
hive> select t.person_name, udtf_id, udtf_key, udtf_value
    > from (
    >     select person_name, string_field 
    >     from  t16
    > ) t LATERAL VIEW udf_wordsplitsinglerow(t.string_field) v as  udtf_id, udtf_key, udtf_value;
OK
t.person_name	udtf_id	udtf_key	udtf_value
tom	1	province	guangdong
jerry	2	city	shenzhen
john	3	can not split to valid array	-
Time taken: 0.122 seconds, Fetched: 3 row(s)

一列拆成多行(每行多列)

  • 前面我們們試過了將string_field欄位拆分成idkeyvalue三個欄位,不過拆分後總行數還是不變,接下來的UDTF,是把string_field拆分成多條記錄,然後每條記錄都有三個欄位;
  • 需要匯入新的資料到t16表,新建文字檔案016_multi.txt,內容如下:
tom|1:province:guangdong,4:city:yangjiang
jerry|2:city:shenzhen
john|3
  • 在hive會話視窗執行以下命令,會用016_multi.txt的內容覆蓋t16表已有內容:
load data 
local inpath '/home/hadoop/temp/202010/25/016_multi.txt' 
overwrite into table t16;
  • 此時的資料如下圖所示,紅框中是一條記錄的string_field欄位值,我們們接下來要開發的UDTF,會先用逗號分隔,得到的就是1:province:guangdong4:city:yangjiang這兩個字串,接下來對每個字串用冒號分隔,就會得到兩條idkeyvalue這樣的記錄,也就是多行多列:

在這裡插入圖片描述

  • 預期中的UDTF結果如下圖所示,紅框和黃框這兩條記錄都來自一條記錄的string_field欄位值:

在這裡插入圖片描述

  • 接下來開始編碼,新建WordSplitMultiRow.java,程式碼如下,可見和WordSplitSingleRow的差異僅在process方法,WordSplitMultiRow的process中執行了多次forward,因此有了多條記錄:
package com.bolingcavalry.hiveudf.udtf;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.exec.UDFArgumentLengthException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDTF;
import org.apache.hadoop.hive.serde2.objectinspector.*;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description: 把指定欄位拆成多行,每行有多列
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2020/11/5 14:43
 */
public class WordSplitMultiRow extends GenericUDTF {

    private PrimitiveObjectInspector stringOI = null;


    private final static String[] EMPTY_ARRAY = {"NULL", "NULL", "NULL"};

    /**
     * 一列拆成多列的邏輯在此
     * @param args
     * @throws HiveException
     */
    @Override
    public void process(Object[] args) throws HiveException {
        String input = stringOI.getPrimitiveJavaObject(args[0]).toString();

        // 無效字串
        if(StringUtils.isBlank(input)) {
            forward(EMPTY_ARRAY);
        } else {

            // 用逗號分隔
            String[] rowArray = input.split(",");

            // 處理異常
            if(null==rowArray || rowArray.length<1) {
                String[] errRlt = new String[3];
                errRlt[0] = input;
                errRlt[1] = "can not split to valid row array";
                errRlt[2] = "-";

                forward(errRlt);
            } else {
                // rowArray的每個元素,都是"id:key:value"這樣的字串
                for(String singleRow : rowArray) {

                    // 要確保字串有效
                    if(StringUtils.isBlank(singleRow)) {
                        forward(EMPTY_ARRAY);
                    } else {
                        // 分割字串
                        String[] array = singleRow.split(":");

                        // 如果字串陣列不合法,就返回原始字串和錯誤提示
                        if(null==array || array.length<3) {
                            String[] errRlt = new String[3];
                            errRlt[0] = input;
                            errRlt[1] = "can not split to valid array";
                            errRlt[2] = "-";

                            forward(errRlt);
                        } else {
                            forward(array);
                        }
                    }
                }

            }
        }
    }

    /**
     * 釋放資源在此執行,本例沒有資源需要釋放
     * @throws HiveException
     */
    @Override
    public void close() throws HiveException {

    }

    @Override
    public StructObjectInspector initialize(StructObjectInspector argOIs) throws UDFArgumentException {

        List<? extends StructField> inputFields = argOIs.getAllStructFieldRefs();

        // 當前UDTF只處理一個引數,在此判斷傳入的是不是一個引數
        if (1 != inputFields.size()) {
            throw new UDFArgumentLengthException("ExplodeMap takes only one argument");
        }

        // 此UDTF只處理字串型別
        if(!Category.PRIMITIVE.equals(inputFields.get(0).getFieldObjectInspector().getCategory())) {
            throw new UDFArgumentException("ExplodeMap takes string as a parameter");
        }

        stringOI = (PrimitiveObjectInspector)inputFields.get(0).getFieldObjectInspector();

        //列名集合
        ArrayList<String> fieldNames = new ArrayList<String>();

        //列對應的value值
        ArrayList<ObjectInspector> fieldOIs = new ArrayList<ObjectInspector>();

        // 第一列的列名
        fieldNames.add("id");
        // 第一列的inspector型別為string型
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        // 第二列的列名
        fieldNames.add("key");
        // 第二列的inspector型別為string型
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        // 第三列的列名
        fieldNames.add("value");
        // 第三列的inspector型別為string型
        fieldOIs.add(PrimitiveObjectInspectorFactory.javaStringObjectInspector);

        return ObjectInspectorFactory.getStandardStructObjectInspector(fieldNames, fieldOIs);
    }
}

驗證UDTF

接下來將WordSplitMultiRow.java部署成臨時函式並驗證;

  1. 編碼完成後,在pom.xml所在目錄執行命令mvn clean package -U
  2. 在target目錄得到檔案hiveudf-1.0-SNAPSHOT.jar
  3. 將jar下載到hive伺服器,我這裡放在此目錄:/home/hadoop/udf/
  4. 如果還在同一個hive會話模式,需要先清理掉之前的jar和函式:
drop temporary function if exists udf_wordsplitsinglerow;
delete jar /home/hadoop/udf/hiveudf-1.0-SNAPSHOT.jar;
  1. 在hive會話模式執行以下命令新增本地jar:
add jar /home/hadoop/udf/hiveudf-1.0-SNAPSHOT.jar;
  1. 部署臨時函式:
create temporary function udf_wordsplitmultirow as 'com.bolingcavalry.hiveudf.udtf.WordSplitMultiRow';
  1. 執行以下SQL驗證:
select udf_wordsplitmultirow(string_field) from t16;
  1. 結果如下,可見每一行記錄的string_field欄位都被分割成了id、key、value三個欄位:
hive> select udf_wordsplitmultirow(string_field) from t16;
OK
id	key	value
1	province	guangdong
4	city	yangjiang
2	city	shenzhen
3	can not split to valid array	-
Time taken: 0.041 seconds, Fetched: 4 row(s)
  1. LATERAL VIEW語法嘗試將其他欄位也查出來,SQL如下:
select t.person_name, udtf_id, udtf_key, udtf_value
from (
    select person_name, string_field 
    from  t16
) t LATERAL VIEW udf_wordsplitmultirow(t.string_field) v as  udtf_id, udtf_key, udtf_value;
  1. 結果如下,符合預期:
hive> select t.person_name, udtf_id, udtf_key, udtf_value
    > from (
    >     select person_name, string_field 
    >     from  t16
    > ) t LATERAL VIEW udf_wordsplitmultirow(t.string_field) v as  udtf_id, udtf_key, udtf_value;
OK
t.person_name	udtf_id	udtf_key	udtf_value
tom	1	province	guangdong
tom	4	city	yangjiang
jerry	2	city	shenzhen
john	3	can not split to valid array	-
Time taken: 0.056 seconds, Fetched: 4 row(s)
  • 至此,HIVE的三種使用者自定義函式我們們都學習和實踐完成了,希望這些內容能給您的實踐帶來一些參考;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章