Hive函數重要應用案例

第一章、多字節分隔符

1.1 應用場景

Hive中的分隔符

Hive中默認使用單字節分隔符來加載文本數據,例如逗號、製表符、空格等等,默認的分隔符爲\001。根據不同文件的不同分隔符,我們可以通過在創建表時使用 row format delimited fields terminated by ‘單字節分隔符’ 來指定文件中的分割符,確保正確將表中的每一列與文件中的每一列實現一一對應的關係。

 

特殊數據

在實際工作中,我們遇到的數據往往不是非常規範化的數據,例如我們會遇到以下的兩種情況

情況一:每一行數據的分隔符是多字節分隔符,例如:”||”、“--”等

上圖中每列的分隔符爲||,爲多字節分隔符

情況二:數據的字段中包含了分隔符
 

上圖中每列的分隔符爲空格,但是數據中包含了分割符,時間字段中也有空格

192.168.88.134 [08/Nov/2020:10:44:32 +0800] "GET / HTTP/1.1" 404 951

1.2 問題與需求

問題

基於上述的兩種特殊數據,我們如果使用正常的加載數據的方式將數據加載到表中,就會出以下兩種錯誤:

情況一:加載數據的分隔符爲多字節分隔符

  • 創建表
--如果表已存在就刪除表
drop table if exists singer;
--創建表
create table singer(
 id string,--歌手id
 name string,--歌手名稱
 country string,--國家
 province string,--省份
 gender string,--性別
 works string--作品
)
--指定列的分隔符爲||
row format delimited fields terminated by '||';
  • 加載數據
load data local inpath '/export/data/test01.txt' into table singer;
  • 查看結果
select * from singer;

  • 問題

數據發生了錯位,沒有正確的加載每一列的數據

  • 原因

Hive中默認只支持單字節分隔符,無法識別多字節分隔符

情況二:數據中包含了分隔符


創建表

--如果表存在,就刪除表
drop table if exists apachelog;
--創建表
create table apachelog(
 ip string,      --IP地址
 stime string,    --時間
 mothed string,  --請求方式
 url string,     --請求地址
 policy string,  --請求協議
 stat string,    --請求狀態
 body string     --字節大小
)
--指定列的分隔符爲空格
row format delimited fields terminated by ' ';
  • 加載數據
load data local inpath '/export/data/apache_web_access.log' into table apachelog;
  • 查看結果
select * from apachelog;

  • 問題

時間字段被切分成了兩個字段,後面所有的字段出現了錯位

  • 原因

時間數據中包含了分隔符,導致Hive認爲這是兩個字段,但實際業務需求中,爲一個字段

需求

基於上面兩種情況的測試發現,當數據中出現了多字節分隔符或者數據中的某個字段包含了分隔符,就會導致數據加載錯位的問題。基於出現的問題,我們需要通過特殊的方法來解決該問題,即使當數據中出現多字節分隔符等情況時,Hive也能正確的加載數據,實現列與數據的一一對應。

1.3 解決方案一:替換分隔符

方案概述

面對情況一,如果數據中的分隔符是多字節分隔符,可以使用程序提前將數據中的多字節分隔符替換爲單字節分隔符,然後使用Hive加載,就可以實現正確加載對應的數據。
例如:原始數據中的分隔符爲“||”
 

程序開發

可以在ETL階段通過一個MapReduce程序,將“||”替換爲單字節的分隔符“|”,示例程序如下:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
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.lib.input.FileInputFormat;
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 ChangeSplitCharMR
 * @Description TODO MapReduce實現將多字節分隔符轉換爲單字節符
 * @Create By  itcast
 */
public class ChangeSplitCharMR extends Configured implements Tool {
    public int run(String[] arg) throws Exception {
        /**
         * 構建Job
         */
        Job job = Job.getInstance(this.getConf(),"changeSplit");
        job.setJarByClass(ChangeSplitCharMR.class);

        /**
         * 配置Job
         */
        //input:讀取需要轉換的文件
        job.setInputFormatClass(TextInputFormat.class);
        Path inputPath = new Path("datas/split/test01.txt");
        FileInputFormat.setInputPaths(job,inputPath);

        //map:調用Mapper
        job.setMapperClass(ChangeSplitMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);

        //reduce:不需要Reduce過程
        job.setNumReduceTasks(0);

        //output
        job.setOutputFormatClass(TextOutputFormat.class);
        Path outputPath = new Path("datas/output/changeSplit");
        TextOutputFormat.setOutputPath(job,outputPath);

        /**
         * 提交Job
         */
        return job.waitForCompletion(true) ? 0 : -1;
    }

    //程序入口
    public static void main(String[] args) throws Exception {
        //調用run
        Configuration conf = new Configuration();
        int status = ToolRunner.run(conf, new ChangeSplitCharMR(), args);
        System.exit(status);
    }

    public static class ChangeSplitMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
        //定義輸出的Key
        private Text outputKey = new Text();
        //定義輸出的Value
        private NullWritable outputValue = NullWritable.get();

        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //獲取每條數據
            String line = value.toString();
            //將裏面的||轉換爲|
            String newLine = line.replaceAll("\\|\\|", "|");
            //替換後的內容作爲Key
            this.outputKey.set(newLine);
            //輸出結果
            context.write(this.outputKey,this.outputValue);
        }
    }
}

這樣做是直接解決數據的問題,雙豎線變單豎線,而沒有解決單字節分隔符的問題。

程序執行結果如下:
 

重新建表加載數據

  • 重新創建Hive表
--如果表已存在就刪除表
drop table if exists singer;
--創建表
create table singer(
 id string,--歌手id
 name string,--歌手名稱
 country string,--國家
 province string,--省份
 gender string,--性別
 works string--作品
)
--指定列的分隔符爲||
row format delimited fields terminated by '|';
  • 在Hive中重新加載數據
load data local inpath '/export/data/part-m-00000' into table singer;

查看結果

總結

在ETL階段可以直接對數據進行分隔符的替換,通過替換分隔符將多字節分隔符更改爲單字節分隔符,就可以解決數據加載的問題,但是這種方式有對應的優缺點,並不是所有的場景適用於該方法。

  • 優點:實現方式較爲簡單,基於字符串替換即可
  • 缺點:無法滿足情況2的需求

1.4 解決方案二:RegexSerDe正則加載

方案概述

面對情況一和情況二的問題,Hive中提供了一種特殊的方式來解決,Hive提供了一種特殊的Serde來加載特殊數據的問題,使用正則匹配來加載數據,匹配每一列的數據。

官網地址:https://cwiki.apache.org/confluence/display/Hive/GettingStarted#GettingStarted-ApacheWeblogData

什麼是SerDe?

Hive的SerDe提供了序列化和反序列化兩個功能,SerDe是英文Serialize和Deserilize的組合縮寫,用於實現將Hive中的對象進行序列化和將數據進行反序列化。

Serialize就是序列化,用於將Hive中使用的java object轉換成能寫入hdfs的字節序列,或者其他系統能識別的流文件。Hive中的insert語句用於將數據寫入HDFS,所以就會調用序列化實現。Hive中的調用過程如下:

Deserilize就是反序列化,用於將字符串或者二進制數據流轉換成Hive能識別的java object對象。所有Hive中的Select語句在查詢數據時,需要將HDFS中的數據解析爲Hive中對象,就需要進行反序列化。Hive可以方便的將數據加載到表中而不需要對數據進行轉換,這樣在處理海量數據時可以節省大量的時間。Hive中的調用過程如下:

Hive中包含的SerDe

官網地址:https://cwiki.apache.org/confluence/display/Hive/SerDe

Hive中默認提供了多種SerDe用於解析和加載不同類型的數據文件,常用的有ORCSerde 、RegexSerde、JsonSerDe等。

RegexSerDe的功能

RegexSerde是Hive中專門爲了滿足複雜數據場景所提供的正則加載和解析數據的接口,使用RegexSerde可以指定正則表達式加載數據,根據正則表達式匹配每一列數據。上述過程中遇到的情況一和情況二的問題,都可以通過RegexSerDe使用正則表達式來加載實現。

RegexSerDe解決多字節分隔符

分析數據格式,構建正則表達式

  • 原始數據格式

01||周杰倫||中國||臺灣||男||七里香

  •  正則表達式定義每一列

([0-9]*)\\|\\|(.*)\\|\\|(.*)\\|\\|(.*)\\|\\|(.*)\\|\\|(.*)

  • 正則校驗

 這裏使用菜鳥教程的工具:https://c.runoob.com/front-end/854/

  •  基於正則表達式,使用RegexSerde建表
--如果表已存在就刪除表
drop table if exists singer;
--創建表
create table singer(
 id string,--歌手id
 name string,--歌手名稱
 country string,--國家
 province string,--省份
 gender string,--性別
 works string--作品
)
--指定使用RegexSerde加載數據
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
--指定正則表達式
WITH SERDEPROPERTIES (
  "input.regex" = "([0-9]*)\\|\\|([^}]*)\\|\\|([^}]*)\\|\\|([^}]*)\\|\\|([^}]*)\\|\\|([^}]*)"
);
  • 加載數據
load data local inpath '/export/data/test01.txt' into table singer;
  • 查看數據結果
select * from singer;

 每一列的數據都被正常的加載,沒有錯位

RegexSerDe解決數據中包含分割符

分析數據格式,構建正則表達式

  • 原始數據格式

192.168.88.100 [08/Nov/2020:10:44:33 +0800] "GET /hpsk_sdk/index.html HTTP/1.1" 200 328

 

  • 正則表達式定義每一列

([^ ]*) ([^}]*) ([^ ]*) ([^ ]*) ([^ ]*) ([0-9]*) ([^ ]*)

  • 正則校驗

  • 基於正則表達式,使用RegexSerde建表
--如果表存在,就刪除表
drop table if exists apachelog;
--創建表
create table apachelog(
 ip string,      --IP地址
 stime string,    --時間
 mothed string,  --請求方式
 url string,     --請求地址
 policy string,  --請求協議
 stat string,    --請求狀態
 body string     --字節大小
)
--指定使用RegexSerde加載數據
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
--指定正則表達式
WITH SERDEPROPERTIES (
  "input.regex" = "([^ ]*) ([^}]*) ([^ ]*) ([^ ]*) ([^ ]*) ([0-9]*) ([^ ]*)"
);
  • 加載數據
load data local inpath '/export/data/apache_web_access.log' into table apachelog;
  • 查看數據結果
select ip,stime,url,stat,body from apachelog;

 時間字段不再被分割爲兩個字段,整體作爲一個字段被加載

總結    

RegexSerde使用簡單,對於各種複雜的數據場景,都可以通過正則定義匹配每行中的每個字段,基本上可以滿足大多數場景的需求,工作中推薦使用該方式來實現對於複雜數據的加載。

1.5 解決方案三:自定義InputFormat

方案概述

Hive中也允許使用自定義InputFormat來解決以上問題,通過在自定義InputFormat,來自定義解析邏輯實現讀取每一行的數據。

自定義InputFormat

自定義InputFormat繼承自TextInputFormat,讀取數據時將每條數據中的”||”全部替換成“|”

  • 自定義InputFormat
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.*;

import java.io.IOException;

/**
 * @ClassName UserInputFormat
 * @Description TODO 用於實現自定義InputFormat,讀取每行數據
 * @Create By     Itcast
 */

public class UserInputFormat extends TextInputFormat {
    @Override
    public RecordReader<LongWritable, Text> getRecordReader(InputSplit genericSplit, JobConf job,
                                                            Reporter reporter) throws IOException {
        reporter.setStatus(genericSplit.toString());
        UserRecordReader reader = new UserRecordReader(job,(FileSplit)genericSplit);
        return reader;
    }
}
  • 自定義RecordReader
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.Seekable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.*;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.mapred.LineRecordReader;
import org.apache.hadoop.mapred.RecordReader;

import java.io.IOException;
import java.io.InputStream;

/**
 * @ClassName UserRecordReader
 * @Description TODO 用於自定義讀取器,在自定義InputFormat中使用,將讀取到的每行數據中的||替換爲|
 * @Create By     Itcast
 */


public class UserRecordReader implements RecordReader<LongWritable, Text> {
    private static final Log LOG = LogFactory.getLog(LineRecordReader.class.getName());
    int maxLineLength;
    private CompressionCodecFactory compressionCodecs = null;
    private long start;
    private long pos;
    private long end;
    private LineReader in;
    private Seekable filePosition;
    private CompressionCodec codec;
    private Decompressor decompressor;

    public UserRecordReader(Configuration job, FileSplit split) throws IOException {
        this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength", Integer.MAX_VALUE);
        start = split.getStart();
        end = start + split.getLength();
        final Path file = split.getPath();
        compressionCodecs = new CompressionCodecFactory(job);
        codec = compressionCodecs.getCodec(file);
        FileSystem fs = file.getFileSystem(job);
        FSDataInputStream fileIn = fs.open(split.getPath());
        if (isCompressedInput()) {
            decompressor = CodecPool.getDecompressor(codec);
            if (codec instanceof SplittableCompressionCodec) {
                final SplitCompressionInputStream cIn = ((SplittableCompressionCodec) codec)
                        .createInputStream(fileIn, decompressor, start, end,
                                SplittableCompressionCodec.READ_MODE.BYBLOCK);
                in = new LineReader(cIn, job);
                start = cIn.getAdjustedStart();
                end = cIn.getAdjustedEnd();
                filePosition = cIn; // take pos from compressed stream
            } else {
                in = new LineReader(codec.createInputStream(fileIn, decompressor), job);
                filePosition = fileIn;
            }
        } else {
            fileIn.seek(start);
            in = new LineReader(fileIn, job);
            filePosition = fileIn;
        }
        if (start != 0) {
            start += in.readLine(new Text(), 0, maxBytesToConsume(start));
        }
        this.pos = start;
    }

    private boolean isCompressedInput() {
        return (codec != null);
    }

    private int maxBytesToConsume(long pos) {
        return isCompressedInput() ? Integer.MAX_VALUE : (int) Math.min(Integer.MAX_VALUE, end - pos);
    }

    private long getFilePosition() throws IOException {
        long retVal;
        if (isCompressedInput() && null != filePosition) {
            retVal = filePosition.getPos();
        } else {
            retVal = pos;
        }
        return retVal;
    }

    public LongWritable createKey() {
        return new LongWritable();
    }

    public Text createValue() {
        return new Text();
    }

    /**
     * Read a line.
     */
    public synchronized boolean next(LongWritable key, Text value) throws IOException {
        while (getFilePosition() <= end) {
            key.set(pos);
            int newSize = in.readLine(value, maxLineLength, Math.max(maxBytesToConsume(pos), maxLineLength));
            String str = value.toString().replaceAll("\\|\\|", "\\|");
            value.set(str);
            pos += newSize;
            if (newSize == 0) {
                return false;
            }
            if (newSize < maxLineLength) {
                return true;
            }
            LOG.info("Skipped line of size " + newSize + " at pos " + (pos - newSize));
        }
        return false;
    }

    public float getProgress() throws IOException {
        if (start == end) {
            return 0.0f;
        } else {
            return Math.min(1.0f, (getFilePosition() - start) / (float) (end - start));
        }
    }

    public synchronized long getPos() throws IOException {
        return pos;
    }

    public synchronized void close() throws IOException {
        try {
            if (in != null) {
                in.close();
            }
        } finally {
            if (decompressor != null) {
                CodecPool.returnDecompressor(decompressor);
            }
        }
    }

    public static class LineReader extends org.apache.hadoop.util.LineReader {
        LineReader(InputStream in) {
            super(in);
        }

        LineReader(InputStream in, int bufferSize) {
            super(in, bufferSize);
        }

        public LineReader(InputStream in, Configuration conf) throws IOException {
            super(in, conf);
        }
    }
}

基於自定義Input創建表

  • 將開發好的InputFormat打成jar包,放入Hive的lib目錄中

  • 在Hive中,將jar包添加到環境變量中
add jar /export/server/hive-3.1.2-bin/lib/HiveUserInputFormat.jar;

該方法可以實現臨時添加,如果希望永久生效,重啓Hive即可

  • 創建表,指定自定義的InputFormat讀取數據
--如果表已存在就刪除表
drop table if exists singer;
--創建表
create table singer(
 id string,--歌手id
 name string,--歌手名稱
 country string,--國家
 province string,--省份
 gender string,--性別
 works string--作品
)
--指定使用分隔符爲|
row format delimited fields terminated by '|'
stored as
--指定使用自定義的類實現解析
inputformat 'bigdata.itcast.cn.hive.mr.UserInputFormat'
outputformat 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat';
  • 加載數據
load data local inpath '/export/data/test01.txt' into table singer;

查看結果

select * from singer;

數據正常匹配,沒有出現錯位。

1.6  總結

當數據文件中出現多字節分隔符或者數據中包含了分隔符時,會導致數據加載與實際表的字段不匹配的問題,基於這個問題我們提供了三種方案:

  • 替換分隔符
  • 正則加載
  • 自定義InputFormat

其中替換分隔符無法解決數據中存在分隔符的問題,自定義InputFormat的開發成本較高,所以整體推薦使用正則加載的方式來實現對於特殊數據的處理。

第二章 URL解析函數及側視圖

2.1 實際工作需求

業務需求中,我們經常需要對用戶的訪問、用戶的來源進行分析,用於支持運營和決策。例如我們經常對用戶訪問的頁面進行統計分析,分析熱門受訪頁面的Top10,觀察大部分用戶最喜歡的訪問最多的頁面等:
 

 
又或者我們需要分析不同搜索平臺的用戶來源分析,統計不同搜索平臺中進入網站的用戶個數,根據數據進行精準的引導和精準的廣告投放等:
 

要想實現上面的受訪分析、來源分析等業務,必須在實際處理數據的過程中,對用戶訪問的URL和用戶的來源URL進行解析處理,獲取用戶的訪問域名、訪問頁面、用戶數據參數、來源域名、來源路徑等信息。
 n

2.2  URL的基本組成

在對URL進行解析時,我們要先了解URL的基本組成部分,再根據實際的需求從URL中獲取對應的部分,例如一條URL由以下幾個部分組成:
 

  • PROTOCOL:協議類型

通信協議類型,一般也叫作Schema,常見的有http、https等;

  • HOST:域名

一般爲服務器的域名主機名或ip地址

  • PATH:訪問路徑

訪問路徑目錄,由“/”隔開的字符串,表示的是主機上的目錄或文件地址

  • QUERY:參數數據

查詢參數,此項爲可選項,可以給動態網頁傳遞參數,用“&”隔開,每個參數的名和值用“=”隔開

2.3  Hive中的URL解析函數

數據準備

Hive中爲了實現對URL的解析,專門提供瞭解析URL的函數parse_url和parse_url_tuple,在show functions中可以看到對應函數

爲了更好的學習這兩個函數的使用,下面我們在Hive中創建一張表,加載url數據來進行測試。

  • 準備數據
vim /export/data/url.txt

添加以下數據內容

1    http://facebook.com/path/p1.php?query=1
2    http://tongji.baidu.com/news/index.jsp?uuid=frank
3    http://www.jdwz.com/index?source=baidu
4    http://www.itcast.cn/index?source=alibaba
  • 創建數據庫
/*創建數據庫*/
create database if not exists db_function;
/*切換數據庫*/
use db_function;
  • 創建表
/*創建數據表*/
create table tb_url(
   id int,
   url string
) row format delimited fields terminated by '\t';
  • 加載數據
/*加載數據*/
load data local inpath '/export/data/url.txt' into table tb_url;
  • 查看數據
/*查詢數據*/
select * from tb_url;

需求

基於當前的數據,實現對URL進行分析,從URL中獲取每個ID對應HOST、PATH以及QUERY,最終實現效果如下:

parse_url

  • 功能

parse_url函數是Hive中提供的最基本的url解析函數,可以根據指定的參數,從URL解析出對應的參數值進行返回,函數爲普通的一對一函數類型。

  • 語法
parse_url(url, partToExtract[, key]) - extracts a part from a URL
  Parts: HOST, PATH, QUERY, REF, PROTOCOL, AUTHORITY, FILE, USERINFO key 

parse_url在使用時需要指定兩個參數

第一個參數:url:指定要解析的URL

第二個參數:key:指定要解析的內容

  • 示例
SELECT parse_url('http://facebook.com/path/p1.php?query=1', 'HOST') FROM src LIMIT 1;
'facebook.com'

SELECT parse_url('http://facebook.com/path/p1.php?query=1', 'QUERY') FROM src LIMIT 1;
'query=1'

SELECT parse_url('http://facebook.com/path/p1.php?query=1', 'QUERY', 'query') FROM src LIMIT 1;
'1'  
  • 測試

查詢tb_url中每個url的HOST

select id,url,parse_url(url,"HOST") as host from tb_url;

查詢tb_url中每個url的PATH

select id,url,parse_url(url,"PATH") as path from tb_url;

查詢tb_url中每個url的QUERY

select id,url,parse_url(url,"QUERY") as query from tb_url;

  • 實現需求
select
   id,
   parse_url(url,"HOST") as host,
   parse_url(url,"PATH") as path,
   parse_url(url,"QUERY") as query
from
  tb_url;

  • 問題

使用parse_url函數每次只能解析一個參數,導致需要經過多個函數調用才能構建多列,開發角度較爲麻煩,實現過程性能也相對較差,需要對同一列做多次計算處理,我們希望能實現調用一次函數,就可以將多個參數進行解析,得到多列結果。

parse_url_tuple

  • 功能

parse_url_tuple函數是Hive中提供的基於parse_url的url解析函數,可以通過一次指定多個參數,從URL解析出多個參數的值進行返回多列,函數爲特殊的一對多函數類型,即通常所說的UDTF函數類型。

  • 語法
parse_url_tuple(url, partname1, partname2, ..., partnameN) - extracts N (N>=1) parts from a URL.
It takes a URL and one or multiple partnames, and returns a tuple. All the input parameters and output column types are string.
Partname: HOST, PATH, QUERY, REF, PROTOCOL, AUTHORITY, FILE, USERINFO, QUERY:<KEY_NAME>

parse_url在使用時可以指定多個參數
第一個參數:url:指定要解析的URL
第二個參數:key1:指定要解析的內容1
……
第N個參數:keyN:指定要解析的內容N

  • 示例
SELECT b.* FROM src LATERAL VIEW parse_url_tuple(fullurl, 'HOST', 'PATH', 'QUERY', 'QUERY:id') b as host, path, query, query_id LIMIT 1;

SELECT parse_url_tuple(a.fullurl, 'HOST', 'PATH', 'QUERY', 'REF', 'PROTOCOL', 'FILE',  'AUTHORITY', 'USERINFO', 'QUERY:k1') as (ho, pa, qu, re, pr, fi, au, us, qk1) from src a; 
  • 測試

查詢tb_url中每個url的HOST、PATH

select parse_url_tuple(url,"HOST","PATH") as (host,path) from tb_url;

查詢tb_url中每個url的PROTOCOL、HOST、QUERY

select parse_url_tuple(url,"PROTOCOL","HOST","PATH") as (protocol,host,path) from tb_url;

  • 實現需求
select parse_url_tuple(url,"HOST","PATH","QUERY") as (host,path,query) from tb_url;

  • 問題

當前實現的過程中,通過parse_url_tuple實現了通過調用一個函數,就可以從URL中解析得到多個參數的值,但是當我們將原表的字段放在一起查詢時,會出現以下問題:

select
id,
parse_url_tuple(url,"HOST","PATH","QUERY") as (host,path,query)
from tb_url;

0: jdbc:hive2://node1:10000> select
. . . . . . . . . . . . . .> id,
. . . . . . . . . . . . . .> parse_url_tuple(url,"HOST","PATH","QUERY") as (host,path,query)
. . . . . . . . . . . . . .> from tb_url;
Error: Error while compiling statement: FAILED: SemanticException 3:52 AS clause has an invalid number of aliases. Error encountered near token 'path' (state=42000,code=40000)

原因在於parse_url_tuple是一個UDTF函數,

2.4 UDTF函數的問題

Hive中的一對多的UDTF函數可以實現高效的數據轉換,但是也存在着一些使用中的問題,UDTF函數對於很多場景下有使用限制,例如:select時不能包含其他字段、不能嵌套調用、不能與group by等放在一起調用等等。
UDTF函數的調用方式,主要有以下兩種方式:
方式一:直接在select後單獨使用
方式二:與Lateral View放在一起使用

2.5 Lateral View側視圖

功能

Lateral View是一種特殊的語法,主要用於搭配UDTF類型功能的函數一起使用,用於解決UDTF函數的一些查詢限制的問題。

側視圖的原理是將UDTF的結果構建成一個類似於視圖的表,然後將原表中的每一行和UDTF函數輸出的每一行進行連接,生成一張新的虛擬表。這樣就避免了UDTF的使用限制問題。使用lateral view時也可以對UDTF產生的記錄設置字段名稱,產生的字段可以用於group by、order by 、limit等語句中,不需要再單獨嵌套一層子查詢。

一般只要使用UDTF,就會固定搭配lateral view使用。
官方鏈接:https://cwiki.apache.org/confluence/display/Hive/LanguageManual+LateralView

語法

lateralView: LATERAL VIEW udtf(expression) tableAlias AS columnAlias (',' columnAlias)*
fromClause: FROM baseTable (lateralView)*

基本語法如下

select …… from tabelA lateral view UDTF(xxx) 別名 as col1,col2,col3……

測試

  • 單個lateral view調用,實現上述需求中的應用
select
  a.id as id,
  b.host as host,
  b.path as path,
  b.query as query
from tb_url a
lateral view parse_url_tuple(url,"HOST","PATH","QUERY") b as host,path,query;

  • 多lateral view調用
select
  a.id as id,
  b.host as host,
  b.path as path,
  c.protocol as protocol,
  c.query as query
from tb_url a
lateral view parse_url_tuple(url,"HOST","PATH") b as host,path
lateral view parse_url_tuple(url,"PROTOCOL","QUERY") c as protocol,query;

  • Outer Lateral View

如果UDTF不產生數據時,這時側視圖與原表關聯的結果將爲空,如下圖所示:

select
  id,
  url,
  col1
from tb_url
lateral view explode(array()) et as col1;

如果加上outer關鍵字以後,就會保留原表數據,類似於outer join

select
  id,
  url,
  col1
from tb_url
lateral view outer explode(array()) et as col1;

第三章 行列轉換應用與實現

3.1 工作應用場景

實際工作場景中經常需要實現對於Hive中的表進行行列轉換操作,例如當前ADS層的數據表,我們統計得到每個小時不同維度下的UV、PV、IP的個數,而現在爲了構建可視化報表,得到每個小時的UV、PV的線圖,觀察訪問趨勢,我們需要構建如下的表結構:
 

 在Hive中,我們可以通過函數來實現各種複雜的行列轉換。

3.2 行轉列:多行轉多列

需求

  • 原始數據表

  • 目標結果表

case when判斷

  • 功能

用於實現對數據的判斷,根據條件,不同的情況返回不同的結果,類似於Java中的switch case 功能

  • 語法

語法一

CASE
WHEN 條件1 THEN VALUE1
WHEN 條件2 THEN VALUE2
……
WHEN 條件N THEN VALUEN
ELSE 默認值
END

語法二

CASEWHEN V1 THEN VALUE1
WHEN V2 THEN VALUE2
……
WHEN VN THEN VALUEN
ELSE 默認值
END
  • 測試

語法一:當id < 2顯示a,當id = 2 顯示b ,其他的顯示c

select
  id,
  case
  when id < 2 then 'a

' when id = 2 then 'b' else 'c' end as caseName from tb_url;

  • 語法二:當id =1 顯示a,當id = 2 顯示b ,其他的顯示c
select
  id,
  case id
  when 1 then 'a'
  when 2 then 'b'
  else 'c'
  end as caseName
from tb_url;

實現

  • 創建原始數據表,加載數據
--切換數據庫
use db_function;
--建表
create table row2col1(
   col1 string,
   col2 string,
   col3 int
) row format delimited fields terminated by '\t';
--加載數據到表中
load data local inpath '/export/data/r2c1.txt' into table row2col1;
  • SQL實現轉換
select
  col1 as col1,
  max(case col2 when 'c' then col3 else 0 end) as c,
  max(case col2 when 'd' then col3 else 0 end) as d,
  max(case col2 when 'e' then col3 else 0 end) as e
from
  row2col1
--先分組
group by col1;

3.3 行轉列:多行轉單列

需求

  • 原始數據表

  • 目標數據表

 

 

concat

  • 功能:用於實現字符串拼接,不可指定分隔符
  • 語法
concat(element1,element2,element3……)
  • 測試
select concat("it","cast","And","heima");
+-----------------+
| itcastAndheima  |
+-----------------+
  • 特點:如果任意一個元素爲null,結果就爲null
select concat("it","cast","And",null);
+-------+
| NULL  |
+-------+

concat_ws

  • 功能:用於實現字符串拼接,可以指定分隔符
  • 語法
concat_ws(SplitChar,element1,element2……)
  • 測試
select concat_ws("-","itcast","And","heima");
+-------------------+
| itcast-And-heima  |
+-------------------+
  • 特點:任意一個元素不爲null,結果就不爲null
select concat_ws("-","itcast","And",null);
+-------------+
| itcast-And  |
+-------------+

collect_list

  • 功能:用於將一列中的多行合併爲一行,不進行去重
  • 語法
collect_list(colName)
  • 測試
select collect_list(col1) from row2col1;
+----------------------------+
| ["a","a","a","b","b","b"]  |
+----------------------------+

concat_set

  • 功能:用於將一列中的多行合併爲一行,並進行去重
  • 語法
collect_set(colName)
  • 測試
select collect_set(col1) from row2col1;
+------------+
| ["b","a"]  |
+------------+

實現

  • 創建原始數據表,加載數據
--切換數據庫
use db_function;

--建表
create table row2col2(
   col1 string,
   col2 string,
   col3 int
)row format delimited fields terminated by '\t';

--加載數據到表中
load data local inpath '/export/data/r2c2.txt' into table row2col2;
  • SQL實現轉換
select
  col1,
  col2,
  concat_ws(',', collect_list(cast(col3 as string))) as col3
from
  row2col2
group by
  col1, col2;

3.4 列轉行:多列轉多行

需求

  • 原始數據

  • 目標結果表

 

 

union

  • 功能:將多個select語句結果合併爲一個,且結果去重且排序
  • 語法
select_statement
UNION [DISTINCT]
select_statement
UNION [DISTINCT]
select_statement ...
  • 測試
select 'b','a','c'
union
select 'a','b','c'
union  
select 'a','b','c';

union all

  • 功能:將多個select語句結果合併爲一個,且結果不去重不排序
  • 語法
select_statement UNION ALL select_statement UNION ALL select_statement ...
  • 測試
select 'b','a','c'
union all
select 'a','b','c'
union all  
select 'a','b','c';

實現

  • 創建原始數據表,加載數據
--切換數據庫
use db_function;

--創建表
create table col2row1
(
  col1 string,
  col2 int,
  col3 int,
  col4 int
) row format delimited fields terminated by '\t';

--加載數據
load data local inpath '/export/data/c2r1.txt'  into table col2row1;

SQL實現轉換

select col1, 'c' as col2, col2 as col3 from col2row1
UNION ALL
select col1, 'd' as col2, col3 as col3 from col2row1
UNION ALL
select col1, 'e' as col2, col4 as col3 from col2row1;

4.5 列轉行:單列轉多行

需求

  • 原始數據表

  • 目標結果表

explode

  • 功能:用於將一個集合或者數組中的每個元素展開,將每個元素變成一行
  • 語法
explode( Map | Array)
  • 測試
select explode(split("a,b,c,d",","));

實現

  • 創建原始數據表,加載數據
--切換數據庫
use db_function;

--創建表
create table col2row2(
   col1 string,
   col2 string,
   col3 string
)row format delimited fields terminated by '\t';


--加載數據
load data local inpath '/export/data/c2r2.txt' into table col2row2;
  • SQL實現轉換
select
  col1,
  col2,
  lv.col3 as col3
from
  col2row2
    lateral view
  explode(split(col3, ',')) lv as col3;

第四章 JSON數據處理

4.1 應用場景

JSON數據格式是數據存儲及數據處理中最常見的結構化數據格式之一,很多場景下公司都會將數據以JSON格式存儲在HDFS中,當構建數據倉庫時,需要對JSON格式的數據進行處理和分析,那麼就需要在Hive中對JSON格式的數據進行解析讀取。

例如,當前我們JSON格式的數據如下:
 

每條數據都以JSON形式存在,每條數據中都包含4個字段,分別爲設備名稱【device】、設備類型【deviceType】、信號強度【signal】和信號發送時間【time】,現在我們需要將這四個字段解析出來,在Hive表中以每一列的形式存儲,最終得到以下Hive表:
 

4.2 處理方式

Hive中爲了實現JSON格式的數據解析,提供了兩種解析JSON數據的方式,在實際工作場景下,可以根據不同數據,不同的需求來選擇合適的方式對JSON格式數據進行處理。

方式一:使用JSON函數進行處理
Hive中提供了兩個專門用於解析JSON字符串的函數:get_json_object、json_tuple,這兩個函數都可以實現將JSON數據中的每個字段獨立解析出來,構建成表。

方式二:使用Hive內置的JSON Serde加載數據

Hive中除了提供JSON的解析函數以外,還提供了一種專門用於加載JSON文件的Serde來實現對JSON文件中數據的解析,在創建表時指定Serde,加載文件到表中,會自動解析爲對應的表格式。

4.3 JSON函數:get_json_object

功能

  用於解析JSON字符串,可以從JSON字符串中返回指定的某個對象列的值

語法

  • 語法
get_json_object(json_txt, path) - Extract a json object from path
  • 參數

第一個參數:指定要解析的JSON字符串
第二個參數:指定要返回的字段,通過$.columnName的方式來指定path

  • 特點:每次只能返回JSON對象中一列的值

使用

  • 創建表
--切換數據庫
use db_function;

--創建表
create table tb_json_test1 (
  json string
);
  • 加載數據
--加載數據
load data local inpath '/export/data/device.json' into table tb_json_test1;
  • 查詢數據
select * from tb_json_test1;

  • 獲取設備名稱字段
select
       json,
       get_json_object(json,"$.device") as device
from tb_json_test1;

  • 獲取設備名稱及信號強度字段
select
       --獲取設備名稱
       get_json_object(json,"$.device") as device,
       --獲取設備信號強度
       get_json_object(json,"$.signal") as signal
from tb_json_test1;

  • 實現需求
select
       --獲取設備名稱
       get_json_object(json,"$.device") as device,
       --獲取設備類型
         get_json_object(json,"$.deviceType") as deviceType,
       --獲取設備信號強度
       get_json_object(json,"$.signal") as signal,
       --獲取時間
       get_json_object(json,"$.time") as stime
from tb_json_test1;

4.4  JSON函數:json_tuple

功能

用於實現JSON字符串的解析,可以通過指定多個參數來解析JSON返回多列的值

語法

  • 語法
json_tuple(jsonStr, p1, p2, ..., pn)
like get_json_object, but it takes multiple names and return a tuple
  • 參數

第一個參數:指定要解析的JSON字符串
第二個參數:指定要返回的第1個字段
 ……
第N+1個參數:指定要返回的第N個字段

  • 特點

功能類似於get_json_object,但是可以調用一次返回多列的值。屬於UDTF類型函數

返回的每一列都是字符串類型

一般搭配lateral view使用

使用

  • 獲取設備名稱及信號強度字段
select
       --返回設備名稱及信號強度
       json_tuple(json,"device","signal") as (device,signal)
from tb_json_test1;

  • 實現需求,單獨使用
select
       --解析所有字段
       json_tuple(json,"device","deviceType","signal","time") as (device,deviceType,signal,stime)
from tb_json_test1;

  • 實現需求,搭配側視圖
select
       json,device,deviceType,signal,stime
from tb_json_test1
lateral view json_tuple(json,"device","deviceType","signal","time") b
as device,deviceType,signal,stime;

4.5  JSONSerde

功能

上述解析JSON的過程中是將數據作爲一個JSON字符串加載到表中,再通過JSON解析函數對JSON字符串進行解析,靈活性比較高,但是對於如果整個文件就是一個JSON文件,在使用起來就相對比較麻煩。Hive中爲了簡化對於JSON文件的處理,內置了一種專門用於解析JSON文件的Serde解析器,在創建表時,只要指定使用JSONSerde解析表的文件,就會自動將JSON文件中的每一列進行解析。

使用

  • 創建表
--切換數據庫
use db_function;

--創建表
create table tb_json_test2 (
   device string,
   deviceType string,
   signal double,
   `time` string
 )
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
STORED AS TEXTFILE;
  • 加載數據
load data local inpath '/export/data/device.json' into table tb_json_test2;
  • 查詢數據
select * from tb_json_test2;

4.6 總結

不論是Hive中的JSON函數還是自帶的JSONSerde,都可以實現對於JSON數據的解析,工作中一般根據數據格式以及對應的需求來實現解析。如果數據中每一行只有個別字段是JSON格式字符串,就可以使用JSON函數來實現處理,但是如果數據加載的文件整體就是JSON文件,每一行數據就是一個JSON數據,那麼建議直接使用JSONSerde來實現處理最爲方便

第五章 窗口函數應用實例

5.1 連續登陸用戶

需求

當前有一份用戶登錄數據如下圖所示,數據中有兩個字段,分別是userId和loginTime。
 

userId表示唯一的用戶ID,唯一標識一個用戶,loginTime表示用戶的登錄日期,例如第一條數據就表示A在2021年3月22日登錄了。

現在需要對用戶的登錄次數進行統計,得到連續登陸N(N>=2)天的用戶。

例如統計連續兩天的登錄的用戶,需要返回A和C,因爲A在22/23/24都登錄了,所以肯定是連續兩天登錄,C在22和23號登錄了,所以也是連續兩天登錄的。

例如統計連續三天的登錄的用戶,只能返回A,因爲只有A是連續三天登錄的。

分析

基於以上的需求根據數據尋找規律,要想得到連續登陸用戶,必須找到兩個相同用戶ID的行之間登陸日期之間的關係。

例如:統計連續登陸兩天的用戶,只要用戶ID相等,並且登陸日期之間相差1天即可。基於這個規律,我們有兩種方案可以實現該需求。

方案一:實現表中的數據自連接,構建笛卡爾積,在結果中找到符合條件的id即可

方案二:使用窗口函數來實現

建表

  • 創建表
--切換數據庫
use db_function;

--建表
create table tb_login(
  userid string,
  logintime string
) row format delimited fields terminated by '\t';
  • 創建數據:vim /export/data/login.log
A       2021-03-22
B       2021-03-22
C       2021-03-22
A       2021-03-23
C       2021-03-23
A       2021-03-24
B       2021-03-24
  • 加載數據
load data local inpath '/export/data/login.log' into table tb_login;
  • 查詢數據
select * from tb_login;

方案一:自連接過濾實現

  • 構建笛卡爾積
select
  a.userid as a_userid,
  a.logintime as a_logintime,
  b.userid as b_userid,
  b.logintime as b_logintime
from tb_login a,tb_login b;
  • 查看數據
+-----------+--------------+-----------+--------------+
| A_USERID  | A_LOGINTIME  | B_USERID  | B_LOGINTIME  |
+-----------+--------------+-----------+--------------+
| A         | 2021-03-22   | A         | 2021-03-22   |
| B         | 2021-03-22   | A         | 2021-03-22   |
| C         | 2021-03-22   | A         | 2021-03-22   |
| A         | 2021-03-23   | A         | 2021-03-22   |
| C         | 2021-03-23   | A         | 2021-03-22   |
| A         | 2021-03-24   | A         | 2021-03-22   |
| B         | 2021-03-24   | A         | 2021-03-22   |
| A         | 2021-03-22   | B         | 2021-03-22   |
| B         | 2021-03-22   | B         | 2021-03-22   |
| C         | 2021-03-22   | B         | 2021-03-22   |
| A         | 2021-03-23   | B         | 2021-03-22   |
| C         | 2021-03-23   | B         | 2021-03-22   |
| A         | 2021-03-24   | B         | 2021-03-22   |
| B         | 2021-03-24   | B         | 2021-03-22   |
| A         | 2021-03-22   | C         | 2021-03-22   |
| B         | 2021-03-22   | C         | 2021-03-22   |
| C         | 2021-03-22   | C         | 2021-03-22   |
| A         | 2021-03-23   | C         | 2021-03-22   |
| C         | 2021-03-23   | C         | 2021-03-22   |
| A         | 2021-03-24   | C         | 2021-03-22   |
| B         | 2021-03-24   | C         | 2021-03-22   |
| A         | 2021-03-22   | A         | 2021-03-23   |
| B         | 2021-03-22   | A         | 2021-03-23   |
| C         | 2021-03-22   | A         | 2021-03-23   |
| A         | 2021-03-23   | A         | 2021-03-23   |
| C         | 2021-03-23   | A         | 2021-03-23   |
| A         | 2021-03-24   | A         | 2021-03-23   |
| B         | 2021-03-24   | A         | 2021-03-23   |
| A         | 2021-03-22   | C         | 2021-03-23   |
| B         | 2021-03-22   | C         | 2021-03-23   |
| C         | 2021-03-22   | C         | 2021-03-23   |
| A         | 2021-03-23   | C         | 2021-03-23   |
| C         | 2021-03-23   | C         | 2021-03-23   |
| A         | 2021-03-24   | C         | 2021-03-23   |
| B         | 2021-03-24   | C         | 2021-03-23   |
| A         | 2021-03-22   | A         | 2021-03-24   |
| B         | 2021-03-22   | A         | 2021-03-24   |
| C         | 2021-03-22   | A         | 2021-03-24   |
| A         | 2021-03-23   | A         | 2021-03-24   |
| C         | 2021-03-23   | A         | 2021-03-24   |
| A         | 2021-03-24   | A         | 2021-03-24   |
| B         | 2021-03-24   | A         | 2021-03-24   |
| A         | 2021-03-22   | B         | 2021-03-24   |
| B         | 2021-03-22   | B         | 2021-03-24   |
| C         | 2021-03-22   | B         | 2021-03-24   |
| A         | 2021-03-23   | B         | 2021-03-24   |
| C         | 2021-03-23   | B         | 2021-03-24   |
| A         | 2021-03-24   | B         | 2021-03-24   |
| B         | 2021-03-24   | B         | 2021-03-24   |
+-----------+--------------+-----------+--------------+
  • 保存爲表
create table tb_login_tmp as
select
  a.userid as a_userid,
  a.logintime as a_logintime,
  b.userid as b_userid,
  b.logintime as b_logintime
from tb_login a,tb_login b;
  • 過濾數據:用戶id相同並且登陸日期相差1
select
  a_userid,a_logintime,b_userid,b_logintime
from tb_login_tmp
where a_userid = b_userid
and cast(substr(a_logintime,9,2) as int) - 1 = cast(substr(b_logintime,9,2) as int);

 

  • 統計連續登陸兩天的用戶
select
  distinct a_userid
from tb_login_tmp
where a_userid = b_userid
and cast(substr(a_logintime,9,2) as int) - 1 = cast(substr(b_logintime,9,2) as int);

  • 問題

如果現在需要統計連續3天的用戶個數,如何實現呢?或者說需要統計連續5天、連續7天、連續10天、連續30天登陸的用戶如何進行計算呢?

如果使用自連接的方式會非常的麻煩才能實現統計連續登陸兩天以上的用戶,並且性能很差,所以我們需要使用第二種方式來實現。

方案二:窗口函數實現

窗口函數lead

  • 功能:用於從當前數據中基於當前行的數據向後偏移取值
  • 語法:lead(,N,defautValue)

colName:取哪一列的值
N:向後偏移N行
defaultValue:如果取不到返回的默認值

  • 分析

當前數據中記錄了每個用戶每一次登陸的日期,一個用戶在一天只有1條信息,我們可以基於用戶的登陸信息,找到如下規律:

連續兩天登陸 : 用戶下次登陸時間 = 本次登陸以後的第二天

連續三天登陸 : 用戶下下次登陸時間 = 本次登陸以後的第三天
……依次類推。

我們可以對用戶ID進行分區,按照登陸時間進行排序,通過lead函數計算出用戶下次登陸時間,通過日期函數計算出登陸以後第二天的日期,如果相等即爲連續兩天登錄。

  • 統計連續2天登錄
select
  userid,
  logintime,
  --本次登陸日期的第二天
  date_add(logintime,1) as nextday,
  --按照用戶id分區,按照登陸日期排序,取下一次登陸時間,取不到就爲0
  lead(logintime,1,0) over (partition by userid order by logintime) as nextlogin
from tb_login;

with t1 as (
  select
    userid,
    logintime,
    --本次登陸日期的第二天
      date_add(logintime,1) as nextday,
    --按照用戶id分區,按照登陸日期排序,取下一次登陸時間,取不到就爲0
     lead(logintime,1,0) over (partition by userid order by logintime) as nextlogin
from tb_login )
select distinct userid from t1 where nextday = nextlogin;
  • 統計連續3天登錄
select
  userid,
  logintime,
  --本次登陸日期的第三天
  date_add(logintime,2) as nextday,
  --按照用戶id分區,按照登陸日期排序,取下下一次登陸時間,取不到就爲0
  lead(logintime,2,0) over (partition by userid order by logintime) as nextlogin
from tb_login;

with t1 as (
select
  userid,
  logintime,
  --本次登陸日期的第三天
  date_add(logintime,2) as nextday,
  --按照用戶id分區,按照登陸日期排序,取下下一次登陸時間,取不到就爲0
  lead(logintime,2,0) over (partition by userid order by logintime) as nextlogin
from tb_login )
select distinct userid from t1 where nextday = nextlogin;
  • 統計連續N天登錄
select
  userid,
  logintime,
  --本次登陸日期的第N天
  date_add(logintime,N-1) as nextday,
  --按照用戶id分區,按照登陸日期排序,取下下一次登陸時間,取不到就爲0
  lead(logintime,N-1,0) over (partition by userid order by logintime) as nextlogin
from tb_login;

5.2 級聯累加求和

需求

當前有一份消費數據如下,記錄了每個用戶在每個月的所有消費記錄,數據表中一共有三列:

  • userId:用戶唯一id,唯一標識一個用戶
  • mth:用戶消費的月份,一個用戶可以在一個月多次消費
  • money:用戶每次消費的金額

現在需要基於用戶每個月的多次消費的記錄進行分析,統計得到每個用戶在每個月的消費總金額以及當前累計消費總金額,最後結果如下:

以用戶A爲例:
    A在2021年1月份,共四次消費,分別消費5元、15元、8元、5元,所以本月共消費33元,累計消費33元。
    A在2021年2月份,共兩次消費,分別消費4元、6元,所以本月共消費10元,累計消費43元。

 

分析

如果要實現以上需求,首先要統計出每個用戶每個月的消費總金額,分組實現集合,但是需要按照用戶ID,將該用戶這個月之前的所有月份的消費總金額進行累加實現。該需求可以通過兩種方案來實現:
方案一:分組統計每個用戶每個月的消費金額,然後構建自連接,根據條件分組聚合
方案二:分組統計每個用戶每個月的消費金額,然後使用窗口聚合函數實現

建表

  • 創建表
--切換數據庫
use db_function;

--建表
create table tb_money(
  userid string,
  mth string,
  money int
) row format delimited fields terminated by '\t';
  • 創建數據:vim /export/data/money.tsv
A    2021-01    5
A    2021-01    15
B    2021-01    5
A    2021-01    8
B    2021-01    25
A    2021-01    5
A    2021-02    4
A    2021-02    6
B    2021-02    10
B    2021-02    5
A    2021-03    7
B    2021-03    9
A    2021-03    11
B    2021-03    6
  • 加載數據
load data local inpath '/export/data/money.tsv' into table tb_money;
  • 查詢數據
select * from tb_money;

  • 統計得到每個用戶每個月的消費總金額
create table tb_money_mtn as
select
  userid,
  mth,
  sum(money) as m_money
from tb_money
group by userid,mth;

方案一:自連接分組聚合

  • 基於每個用戶每個月的消費總金額進行自連接
select
  a.userid as auserid,
  a.mth as amth,
  a.m_money as am_money,
  b.userid as buserid,
  b.mth as bmth,
  b.m_money as bm_money
from tb_money_mtn a join tb_money_mtn b on a.userid = b.userid;

  • 將每個月之前月份的數據過濾出來
select
  a.userid as auserid,
  a.mth as amth,
  a.m_money as am_money,
  b.userid as buserid,
  b.mth as bmth,
  b.m_money as bm_money
from tb_money_mtn a join tb_money_mtn b on a.userid = b.userid
where a.mth >= b.mth;

 

 

  •  對每個用戶每個月的金額進行分組,聚合之前月份的消費金額
select
  a.userid as auserid,
  a.mth as amth,
  a.m_money as am_money,
  sum(b.m_money) as t_money
from tb_money_mtn a join tb_money_mtn b on a.userid = b.userid
where a.mth >= b.mth
group by a.userid,a.mth,a.m_money;

方案二:窗口函數實現

  • 窗口函數sum
  • 功能:用於實現基於窗口的數據求和
  • 語法:sum(colName) over (partition by col order by col)

colName:對某一列的值進行求和

  • 分析

基於每個用戶每個月的消費金額,可以通過窗口函數對用戶進行分區,按照月份排序,然後基於聚合窗口,從每個分區的第一行累加到當前和,即可得到累計消費金額。

  • 統計每個用戶每個月消費金額及累計總金額
select
    userid,
    mth,
    m_money,
    sum(m_money) over (partition by userid order by mth) as t_money
from tb_money_mtn;

5.3 分組TopN

需求

工作中經常需要實現TopN的需求,例如熱門商品Top10、熱門話題Top20、熱門搜索Top10、地區用戶Top10等等,TopN是大數據業務分析中最常見的需求。

普通的TopN只要基於數據進行排序,然後基於排序後的結果取前N個即可,相對簡單,但是在TopN中有一種特殊的TopN計算,叫做分組TopN。

分組TopN指的是基於數據進行分組,從每個組內取TopN,不再基於全局取TopN。如果要實現分組取TopN就相對麻煩。

例如:現在有一份數據如下,記錄這所有員工的信息:

如果現在有一個需求:查詢每個部門薪資最高的員工的薪水,這個可以直接基於表中數據分組查詢得到

select deptno,max(salary) from tb_emp group by deptno;

但是如果現在需求修改爲:統計查詢每個部門薪資最高的前兩名員工的薪水,這時候應該如何實現呢?

分析

根據上述需求,這種情況下是無法根據group by分組聚合實現的,因爲分組聚合只能實現返回一條聚合的結果,但是需求中需要每個部門返回薪資最高的前兩名,有兩條結果,這時候就需要用到窗口函數中的分區來實現了。

建表

  • 創建表
--切換數據庫
use db_function;

--建表
create table tb_emp(
   empno string,
   ename string,
   job string,
   managerid string,
   hiredate string,
   salary double,
   bonus double,
   deptno string
) row format delimited fields terminated by '\t';
  • 創建數據:vim /export/data/emp.txt
7369    SMITH    CLERK    7902    1980-12-17    800.00        20
7499    ALLEN    SALESMAN    7698    1981-2-20    1600.00    300.00    30
7521    WARD    SALESMAN    7698    1981-2-22    1250.00    500.00    30
7566    JONES    MANAGER    7839    1981-4-2    2975.00        20
7654    MARTIN    SALESMAN    7698    1981-9-28    1250.00    1400.00    30
7698    BLAKE    MANAGER    7839    1981-5-1    2850.00        30
7782    CLARK    MANAGER    7839    1981-6-9    2450.00        10
7788    SCOTT    ANALYST    7566    1987-4-19    3000.00        20
7839    KING    PRESIDENT        1981-11-17    5000.00        10
7844    TURNER    SALESMAN    7698    1981-9-8    1500.00    0.00    30
7876    ADAMS    CLERK    7788    1987-5-23    1100.00        20
7900    JAMES    CLERK    7698    1981-12-3    950.00        30
7902    FORD    ANALYST    7566    1981-12-3    3000.00        20
7934    MILLER    CLERK    7782    1982-1-23    1300.00        10
  • 加載數據
load data local inpath '/export/data/emp.txt' into table tb_emp;
  • 查詢數據
select empno,ename,salary,deptno from tb_emp;

 

 

實現

  • TopN函數:row_number、rank、dense_rank

row_number:對每個分區的數據進行編號,如果值相同,繼續編號
rank:對每個分區的數據進行編號,如果值相同,編號相同,但留下空位
dense_rank:對每個分區的數據進行編號,如果值相同,編號相同,不留下空位

基於row_number實現,按照部門分區,每個部門內部按照薪水降序排序

select
       empno,
       ename,
       salary,
       deptno,
       row_number() over (partition by deptno order by salary desc) as rn
from tb_emp;

  • 過濾每個部門的薪資最高的前兩名
with t1 as (
select
       empno,
       ename,
       salary,
       deptno,
       row_number() over (partition by deptno order by salary desc) as rn
from tb_emp )
select * from t1 where rn < 3;

第六章 拉鍊表的設計與實現

6. 1 數據同步問題

數據同步的場景


Hive在實際工作中主要用於構建離線數據倉庫,定期的從各種數據源中同步採集數據到Hive中,經過分層轉換提供數據應用。例如,每天需要從MySQL中同步最新的訂單信息、用戶信息、店鋪信息等到數據倉庫中,進行訂單分析、用戶分析。

例如:MySQL中有一張用戶表:tb_user,每個用戶註冊完成以後,就會在用戶表中新增該用戶的信息,記錄該用戶的id、手機號碼、用戶名、性別、地址等信息。

每天都會有用戶註冊,產生新的用戶信息,我們每天都需要將MySQL中的用戶數據同步到Hive數據倉庫中,在做用戶分析時,需要對用戶的信息做統計分析,例如統計新增用戶的個數、總用戶個數、用戶性別分佈、地區分佈、運營商分佈等指標。

數據同步的問題

在實現數據倉庫數據同步的過程中,我們必須保證Hive中的數據與MySQL中的數據是一致的,這樣才能確保我們最終分析出來的結果是準確的,沒有問題的,但是在實現同步的過程中,這裏會面臨一個問題:如果MySQL中的數據發生了修改,Hive中如何存儲被修改的數據?
例如以下情況

  • 2021-01-01:MySQL中有10條用戶信息

  • 2021-01-02:Hive進行數據分析,將MySQL中的數據同步

  • 2021-01-02:MySQL中新增2條用戶註冊數據,並且有1條用戶數據發生更新

新增兩條用戶數據011和012
008的addr發生了更新,從gz更新爲sh

2021-01-03:Hive需要對2號的數據進行同步更新處理
問題:新增的數據會直接加載到Hive表中,但是更新的數據如何存儲在Hive表中?

解決方案

方案一:在Hive中用新的addr覆蓋008的老的addr,直接更新

優點:實現最簡單,使用起來最方便
缺點:沒有歷史狀態,008的地址是1月2號在sh,但是1月2號之前是在gz的,如果要查詢008的1月2號之前的addr就無法查詢,也不能使用sh代替

方案二:每次數據改變,根據日期構建一份全量的快照表,每天一張表

2021-01-02:Hive中有一張表tb_user_2021-01-02

2021-01-03:Hive中有一張表tb_user_2021-01-03

優點:記錄了所有數據在不同時間的狀態
缺點:冗餘存儲了很多沒有發生變化的數據,導致存儲的數據量過大

方案三:構建拉鍊表,通過時間標記發生變化的數據的每種狀態的時間週期

6.2 拉鍊表的設計

功能與應用場景

 

拉鍊表專門用於解決在數據倉庫中數據發生變化如何實現數據存儲的問題,如果直接覆蓋歷史狀態,會導致無法查詢歷史狀態,如果將所有數據單獨切片存儲,會導致存儲大量非更新數據的問題。拉鍊表的設計是將更新的數據進行狀態記錄,沒有發生更新的數據不進行狀態存儲,用於存儲所有數據在不同時間上的所有狀態,通過時間進行標記每個狀態的生命週期,查詢時,根據需求可以獲取指定時間範圍狀態的數據,默認用9999-12-31等最大值來表示最新狀態。

實現過程

整體實現過程一般分爲三步,第一步先增量採集所有新增數據【增加的數據和發生變化的數據】放入一張增量表。第二步創建一張臨時表,用於將老的拉鍊表與增量表進行合併。第三步,最後將臨時表的數據覆蓋寫入拉鍊表中。例如:
當前MySQL中的數據:

當前Hive數據倉庫中拉鍊表的數據:

  • step1:增量採集變化數據,放入增量表中

  • step2:構建臨時表,將Hive中的拉鍊表與臨時表的數據進行合併

  • step3:將臨時表的數據覆蓋寫入拉鍊表中

6.3 拉鍊表的實現

 

數據準備

  • 創建dw層拉鍊表
--創建數據庫
create database db_zipper;
use db_zipper;

--創建拉鍊表
create table dw_zipper(
  userid string,
  phone string,
  nick string,
  gender int,
  addr string,
  starttime string,
  endtime string
) row format delimited fields terminated by '\t';
  • 構建模擬數據:vim /export/data/zipper.txt
001    186xxxx1234    laoda    0    sh    2021-01-01    9999-12-31
002    186xxxx1235    laoer    1    bj    2021-01-01    9999-12-31
003    186xxxx1236    laosan    0    sz    2021-01-01    9999-12-31
004    186xxxx1237    laosi    1    gz    2021-01-01    9999-12-31
005    186xxxx1238    laowu    0    sh    2021-01-01    9999-12-31
006    186xxxx1239    laoliu    1    bj    2021-01-01    9999-12-31
007    186xxxx1240    laoqi    0    sz    2021-01-01    9999-12-31
008    186xxxx1241    laoba    1    gz    2021-01-01    9999-12-31
009    186xxxx1242    laojiu    0    sh    2021-01-01    9999-12-31
010    186xxxx1243    laoshi    1    bj    2021-01-01    9999-12-31
  • 加載拉鍊表數據
--加載模擬數據
load data local inpath '/export/data/zipper.txt' into table dw_zipper;
  • 查詢數據
select userid,nick,addr,starttime,endtime from dw_zipper;

增量採集

 

  • 創建ods層增量表
create table ods_zipper_update(
  userid string,
  phone string,
  nick string,
  gender int,
  addr string,
  starttime string,
  endtime string
) row format delimited fields terminated by '\t';
  • 創建模擬數據:vim /export/data/update.txt
008    186xxxx1241    laoba    1    sh    2021-01-02    9999-12-31
011    186xxxx1244    laoshi    1    jx    2021-01-02    9999-12-31
012    186xxxx1245    laoshi    0    zj    2021-01-02    9999-12-31
  • 加載更新數據
load data local inpath '/export/data/update.txt' into table ods_zipper_update;
  • 查詢數據
select userid,nick,addr,starttime,endtime from ods_zipper_update;

合併數據

  • 創建臨時表
create table tmp_zipper(
  userid string,
  phone string,
  nick string,
  gender int,
  addr string,
  starttime string,
  endtime string
) row format delimited fields terminated by '\t';
  • 合併拉鍊表與增量表
insert overwrite table tmp_zipper
select
  userid,
  phone,
  nick,
  gender,
  addr,
  starttime,
  endtime
from ods_zipper_update
union all
--查詢原來拉鍊表的所有數據,並將這次需要更新的數據的endTime更改爲更新值的startTime
select
  a.userid,
  a.phone,
  a.nick,
  a.gender,
  a.addr,
  a.starttime,
  --如果這條數據沒有更新或者這條數據不是要更改的數據,就保留原來的值,否則就改爲新數據的開始時間-1
  if(b.userid is null or a.endtime < '9999-12-31', a.endtime , date_sub(b.starttime,1)) as endtime
from dw_zipper a  left join ods_zipper_update b
on a.userid = b.userid ;

生成最新拉鍊表

  • 覆蓋拉鍊表
insert overwrite table dw_zipper
select * from tmp_zipper;

 

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