Parquet的那些事(三)嵌套數據模型

在大數據系統中,我們總是不可避免的會遇到嵌套結構的數據。這是因爲,在很多場景下,嵌套數據結構能更好的表達數據內容與層級關係,因此很多數據源會採用這樣的結構來輸出數據。然而,相比關係型的結構化數據,這樣的數據並不利於高效查詢,因此在很多場景下,我們還會通過ETL將其變爲扁平結構的數據來存儲。

2010年,Google發表了論文Dremel: Interactive Analysis of Web-Scale Datasets,闡述了一種針對嵌套數據的交互式查詢系統,爲業界提供了思路。正如官方文檔所述,Parquet在最初設計時,便借鑑了Dremel的數據模型思想,支持嵌套結構的存儲。當然,Parquet只是一種列式存儲格式,要完成類似Dremel的查詢功能,還需要計算引擎的配合。

Parquet is built from the ground up with complex nested data structures in mind, and uses the record shredding and assembly algorithm described in the Dremel paper. We believe this approach is superior to simple flattening of nested name spaces.

本文主要探討Parquet是如何支持嵌套結構存儲的。搞清楚這些,能幫助我們更好的設計數據存儲方式、選擇合適的查詢引擎。本文將採用下面的3條數據來進行闡述,後面用“示例”來代表。另外,文中所述是基於Spark 2.4.0、Parquet 1.10.0的。

Record 1:
{
    "sid":"8509_1576752657",
    "appid":[81, 205, 67],
    "tcp": {
        "mss": 1750,
        "flag": 344
    },
    "trans":[
        {
            "uri":"/icon.jpg",
            "monitor_flag":1
        },
        {
            "uri":"/myyhp_2.2-4.js"
        }
    ]
}

Record 2:
{
    "sid":"8510_1576752667",
    "appid":[58, 98]
}

Record 3:
{
    "sid":"8511_1576754667",
    "appid":[198],
    "tcp": {
        "flag": 256
    }
}

數據模型

我們先來看看嵌套結構的數據具有哪些特性,從而搞清楚其存儲要解決的問題是什麼。

  • 數據具有層級關係,比如示例中的tcp下面有mss和flag
  • 允許部分字段爲空,即沒有定義,比如示例中的trans在第2、3條數據中都沒有
  • 有些字段的值有多個,即是一個數組,比如示例中的appid

在Parquet的世界裏,數據是以列式存儲的,每條數據最終都要轉化成一組列來存儲。那麼,該用什麼模型來表達一個嵌套結構呢?

參考Dremel的方式,針對示例中的數據,可以使用下面的模型來表達。對於一個數據結構,其根爲Record,稱爲message,其內部每個字段包含三部分:字段類型、數據類型、字段名稱。通過數據類型group來表達層級關係;通過將字段類型分爲三種,來表達空或數組的概念。

  • required:exactly one occurrence
  • optional: 0 or 1 occurrence
  • repeated: 0 or more occurrence
message Record {
	required string sid, 
	repeated long appid,
	optional group tcp {
		optional long mss,
		optional long flag
	},
	repeated group trans {
		optional string uri,
		optional int monitor_flag
	}
}

我們將上面的數據模型轉化成樹型關係圖,其所要表達的數據列便會呈現出來。

到這裏,我們可以將示例中的數據轉換成下表所示的形式,來實現列式存儲。然而,新的問題又出現了,我們無法將其恢復到原來的數據行的結構形式。以appid爲例,你不知道這些值裏面哪些是第一行的值,哪些是第二行的,等等。因此,單純依靠一個值來表達是不夠了。Parquet採用了Dremel中(R, D, V)模型,V表示數據值,D和R將在下面分別介紹。


Definition Level

D,即Definition Level,用於表達某個列是否爲空、在哪裏爲空,其值爲當前列在第幾層上有值。對於required的字段,沒有D值。

以示例的trans.monitor_flag爲例。Root爲第0層級,trans爲第1層級,monitor_flag爲第2層級。在第1條數據中,trans有兩個值,第1個值裏面的monitor_flag有值,因此D=2;第2個裏面沒有monitor_flag,因此D=1。在第2條數據中,沒有trans,因此D=0。第3條數據,與第2條數據情況一致。

另一個列tcp.mss的情況如下,讀者可以自行推導。


Repetition Level

R,即Repetition Level,用於表達一個列有重複,即有多個值的情況,其值爲重複是在第幾層上發生。

以示例的appid爲例。Root爲第0層級,appid爲第1層級。對於每條新的數據,第1個appid的值對應的R一定是0;而對於每條數據裏面的多個值,則是用appid所在的層來表示,即R=1。比如,第1條數據有3個appid值,其第1個的R=0,後面兩個的R=1。


整體總結

有了上面的闡述,我們可以對示例的數據進行整理,得到每個列及其相應的(R, D, V)的值。這個數據模型,既滿足了列式存儲,又可以有效的恢復原有的數據行。

但是,如果我們將示例數據通過Spark寫入到Parquet文件,會發現Parquet文件的Schema信息與我們上面的闡述略有差別,主要表現在:

  • 對於sid列,我們期望的是required,而文件Schema裏面是optional
  • 對於appid與trans,文件Schema在對數組的表達上增加了兩層,即.list.element

通過分析,這兩個問題都跟Spark有關。第一個問題,是因爲在Spark中定義schema時,將sid字段的nullable設置爲true了,導致Spark認爲其爲optional。對於required類型字段,應該將其置爲false。

StructField("sid", StringType(), False)

第二個問題,跟Spark的ParquetWriteSupport有關,其在表述Array數據類型時增加了list和element兩層,採用了三層關係的方式。根據這個變化,我們對上述的樹形圖和列值進行了修正,主要變化的是appid和trans下面的D值,對R和V值沒有影響。


結尾

本文從嵌套結構的特性入手,逐步探討了Parquet的嵌套數據模型。正如文章開頭所言,很多場景下我們最關注的是查詢性能,而Parquet只是提供了一種存儲方式,具體的查詢還要依賴生態圈內的計算引擎,比如Spark、Presto等。其性能通常與相應的計算引擎中的Parquet Reader有關,因爲他們決定了能否有效的進行嵌套字段的Column Pruning和Predicate Pushdown Filter。

我們曾經在AWS Athena下做過嵌套數據與扁平數據的查詢性能對比,前者比後者差了60多倍以上,主要表現在查詢時間和加載的數據量上。AWS Athena底層跑的是Presto,因爲是託管服務,我們尚且不知道是Presto的哪個版本,但至少說明這個Presto沒有很好的支持嵌套數據查詢。Spark在2.4.0之前對嵌套數據查詢的支持也很弱。關於這些,我將在後面另起博文來分析Spark和Presto中對嵌套數據查詢的支持情況。



(全文完,本文地址:https://bruce.blog.csdn.net/article/details/105417979

版權聲明:本人拒絕不規範轉載,所有轉載需徵得本人同意,並且不得更改文字與圖片內容。大家相互尊重,謝謝!

Bruce
2020/04/12 晚

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