Parquet僅僅是一種存儲格式,它是語言、平臺無關的,而且不須要和任何一種數據處理框架綁定,目前可以和Parquet適配的組件包括下面這些,能夠看出基本上一般使用的查詢引擎和計算框架都已適配,而且能夠很方便的將其它序列化工具生成的數據轉換成Parquet格式。java
Parquet項目由如下幾個子項目組成:git
下圖展現了Parquet各個組件的層次以及從上到下交互的方式。github
Parquet支持嵌套的數據模型,相似於Protocol Buffers,每個數據模型的schema包含多個字段,每個字段又能夠包含多個字段,每個字段有三個屬性:重複數、數據類型和字段名,重複數能夠是如下三種:required(出現1次),repeated(出現0次或屢次),optional(出現0次或1次)。每個字段的數據類型能夠分紅兩種:group(複雜類型)和primitive(基本類型)。例如Dremel中提供的Document的schema示例,它的定義以下:算法
message Document { required int64 DocId; optional group Links { repeated int64 Backward; repeated int64 Forward; } repeated group Name { repeated group Language { required string Code; optional string Country; } optional string Url; } }
能夠把這個Schema轉換成樹狀結構,根節點能夠理解爲repeated類型,以下圖: 數據庫
能夠看出在Schema中全部的基本類型字段都是葉子節點,在這個Schema中一共存在6個葉子節點,若是把這樣的Schema轉換成扁平式的關係模型,就能夠理解爲該表包含六個列。Parquet中沒有Map、Array這樣的複雜數據結構,可是能夠經過repeated和group組合來實現這樣的需求。在這個包含6個字段的表中有如下幾個字段和每一條記錄中它們可能出現的次數:apache
DocId int64 只能出現一次 Links.Backward int64 可能出現任意屢次,可是若是出現0次則須要使用NULL標識 Links.Forward int64 同上 Name.Language.Code string 同上 Name.Language.Country string 同上 Name.Url string 同上
因爲在一個表中可能存在出現任意屢次的列,對於這些列須要標示出現屢次或者等於NULL的狀況,它是由Striping/Assembly算法實現的。編程
上文介紹了Parquet的數據模型,在Document中存在多個非required列,因爲Parquet一條記錄的數據分散的存儲在不一樣的列中,如何組合不一樣的列值組成一條記錄是由Striping/Assembly算法決定的,在該算法中列的每個值都包含三部分:value、repetition level和definition level。緩存
爲了支持repeated類型的節點,在寫入的時候該值等於它和前面的值在哪一層節點是不共享的。在讀取的時候根據該值能夠推導出哪一層上須要建立一個新的節點,例如對於這樣的一個schema和兩條記錄。數據結構
message nested { repeated group leve1 { repeated string leve2; } } r1:[[a,b,c,] , [d,e,f,g]] r2:[[h] , [i,j]]
計算repetition level值的過程以下:app
根據以上的分析每個value須要記錄的repeated level值以下:
在讀取的時候,順序的讀取每個值,而後根據它的repeated level建立對象,當讀取value=a時repeated level=0,表示須要建立一個新的根節點(新記錄),value=b時repeated level=2,表示須要建立一個新的level2節點,value=d時repeated level=1,表示須要建立一個新的level1節點,當全部列讀取完成以後能夠建立一條新的記錄。本例中當讀取文件構建每條記錄的結果以下:
能夠看出repeated level=0表示一條記錄的開始,而且repeated level的值只是針對路徑上的repeated類型的節點,所以在計算該值的時候能夠忽略非repeated類型的節點,在寫入的時候將其理解爲該節點和路徑上的哪個repeated節點是不共享的,讀取的時候將其理解爲須要在哪一層建立一個新的repeated節點,這樣的話每一列最大的repeated level值就等於路徑上的repeated節點的個數(不包括根節點)。減少repeated level的好處可以使得在存儲使用更加緊湊的編碼方式,節省存儲空間。
有了repeated level咱們就能夠構造出一個記錄了,爲何還須要definition levels呢?因爲repeated和optional類型的存在,可能一條記錄中某一列是沒有值的,假設咱們不記錄這樣的值就會致使本該屬於下一條記錄的值被當作當前記錄的一部分,從而形成數據的錯誤,所以對於這種狀況須要一個佔位符標示這種狀況。
definition level的值僅僅對於空值是有效的,表示在該值的路徑上第幾層開始是未定義的,對於非空的值它是沒有意義的,由於非空值在葉子節點是定義的,全部的父節點也確定是定義的,所以它老是等於該列最大的definition levels。例以下面的schema。
message ExampleDefinitionLevel { optional group a { optional group b { optional string c; } } }
它包含一個列a.b.c,這個列的的每個節點都是optional類型的,當c被定義時a和b確定都是已定義的,當c未定義時咱們就須要標示出在從哪一層開始時未定義的,以下面的值:
因爲definition level只須要考慮未定義的值,而對於repeated類型的節點,只要父節點是已定義的,該節點就必須定義(例如Document中的DocId,每一條記錄都該列都必須有值,一樣對於Language節點,只要它定義了Code必須有值),因此計算definition level的值時能夠忽略路徑上的required節點,這樣能夠減少definition level的最大值,優化存儲。
本節咱們使用Dremel論文中給的Document示例和給定的兩個值r1和r2展現計算repeated level和definition level的過程,這裏把未定義的值記錄爲NULL,使用R表示repeated level,D表示definition level。
首先看DocuId這一列,對於r1,DocId=10,因爲它是記錄的開始而且是已定義的,因此R=0,D=0,一樣r2中的DocId=20,R=0,D=0。
對於Links.Forward這一列,在r1中,它是未定義的可是Links是已定義的,而且是該記錄中的第一個值,因此R=0,D=1,在r1中該列有兩個值,value1=10,R=0(記錄中該列的第一個值),D=2(該列的最大definition level)。
對於Name.Url這一列,r1中它有三個值,分別爲url1=’http://A‘,它是r1中該列的第一個值而且是定義的,因此R=0,D=2;value2=’http://B‘,和上一個值value1在Name這一層是不相同的,因此R=1,D=2;value3=NULL,和上一個值value2在Name這一層是不相同的,因此R=1,但它是未定義的,而Name這一層是定義的,因此D=1。r2中該列只有一個值value3=’http://C‘,R=0,D=2.
最後看一下Name.Language.Code這一列,r1中有4個值,value1=’en-us’,它是r1中的第一個值而且是已定義的,因此R=0,D=2(因爲Code是required類型,這一列repeated level的最大值等於2);value2=’en’,它和value1在Language這個節點是不共享的,因此R=2,D=2;value3=NULL,它是未定義的,可是它和前一個值在Name這個節點是不共享的,在Name這個節點是已定義的,因此R=1,D=1;value4=’en-gb’,它和前一個值在Name這一層不共享,因此R=1,D=2。在r2中該列有一個值,它是未定義的,可是Name這一層是已定義的,因此R=0,D=1.
Parquet文件是以二進制方式存儲的,因此是不能夠直接讀取的,文件中包括該文件的數據和元數據,所以Parquet格式文件是自解析的。在HDFS文件系統和Parquet文件中存在以下幾個概念。
一般狀況下,在存儲Parquet數據的時候會按照Block大小設置行組的大小,因爲通常狀況下每個Mapper任務處理數據的最小單位是一個Block,這樣能夠把每個行組由一個Mapper任務處理,增大任務執行並行度。Parquet文件的格式以下圖所示
上圖展現了一個Parquet文件的內容,一個文件中能夠存儲多個行組,文件的首位都是該文件的Magic Code,用於校驗它是否爲一個Parquet文件,Footer length了文件元數據的大小,經過該值和文件長度能夠計算出元數據的偏移量,文件的元數據中包括每個行組的元數據信息和該文件存儲數據的Schema信息。除了文件中每個行組的元數據,每一頁的開始都會存儲該頁的元數據,在Parquet中,有三種類型的頁:數據頁、字典頁和索引頁。數據頁用於存儲當前行組中該列的值,字典頁存儲該列值的編碼字典,每個列塊中最多包含一個字典頁,索引頁用來存儲當前行組下該列的索引,目前Parquet中還不支持索引頁,可是在後面的版本中增長。
在執行MR任務的時候可能存在多個Mapper任務的輸入是同一個Parquet文件的狀況,每個Mapper經過InputSplit標示處理的文件範圍,若是多個InputSplit跨越了一個Row Group,Parquet可以保證一個Row Group只會被一個Mapper任務處理。
說到列式存儲的優點,映射下推是最突出的,它意味着在獲取表中原始數據時只須要掃描查詢中須要的列,因爲每一列的全部值都是連續存儲的,因此分區取出每一列的全部值就能夠實現TableScan算子,而避免掃描整個表文件內容。
在Parquet中原生就支持映射下推,執行查詢的時候能夠經過Configuration傳遞須要讀取的列的信息,這些列必須是Schema的子集,映射每次會掃描一個Row Group的數據,而後一次性得將該Row Group裏全部須要的列的Cloumn Chunk都讀取到內存中,每次讀取一個Row Group的數據可以大大下降隨機讀的次數,除此以外,Parquet在讀取的時候會考慮列是否連續,若是某些須要的列是存儲位置是連續的,那麼一次讀操做就能夠把多個列的數據讀取到內存。
在數據庫之類的查詢系統中最經常使用的優化手段就是謂詞下推了,經過將一些過濾條件儘量的在最底層執行能夠減小每一層交互的數據量,從而提高性能,例如」select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100」SQL查詢中,在處理Join操做以前須要首先對A和B執行TableScan操做,而後再進行Join,再執行過濾,最後計算聚合函數返回,可是若是把過濾條件A.a > 10和B.b < 100分別移到A表的TableScan和B表的TableScan的時候執行,能夠大大下降Join操做的輸入數據。
不管是行式存儲仍是列式存儲,均可以在將過濾條件在讀取一條記錄以後執行以判斷該記錄是否須要返回給調用者,在Parquet作了更進一步的優化,優化的方法時對每個Row Group的每個Column Chunk在存儲的時候都計算對應的統計信息,包括該Column Chunk的最大值、最小值和空值個數。經過這些統計值和該列的過濾條件能夠判斷該Row Group是否須要掃描。另外Parquet將來還會增長諸如Bloom Filter和Index等優化數據,更加有效的完成謂詞下推。
在使用Parquet的時候能夠經過以下兩種策略提高查詢性能:一、相似於關係數據庫的主鍵,對須要頻繁過濾的列設置爲有序的,這樣在導入數據的時候會根據該列的順序存儲數據,這樣能夠最大化的利用最大值、最小值實現謂詞下推。二、減少行組大小和頁大小,這樣增長跳過整個行組的可能性,可是此時須要權衡因爲壓縮和編碼效率降低帶來的I/O負載。
相比傳統的行式存儲,Hadoop生態圈近年來也涌現出諸如RC、ORC、Parquet的列式存儲格式,它們的性能優點主要體如今兩個方面:一、更高的壓縮比,因爲相同類型的數據更容易針對不一樣類型的列使用高效的編碼和壓縮方式。二、更小的I/O操做,因爲映射下推和謂詞下推的使用,能夠減小一大部分沒必要要的數據掃描,尤爲是表結構比較龐大的時候更加明顯,由此也可以帶來更好的查詢性能
上圖是展現了使用不一樣格式存儲TPC-H和TPC-DS數據集中兩個表數據的文件大小對比,能夠看出Parquet較之於其餘的二進制文件存儲格式可以更有效的利用存儲空間,而新版本的Parquet(2.0版本)使用了更加高效的頁存儲方式,進一步的提高存儲空間
上圖展現了Twitter在Impala中使用不一樣格式文件執行TPC-DS基準測試的結果,測試結果能夠看出Parquet較之於其餘的行式存儲格式有較明顯的性能提高。
上圖展現了criteo公司在Hive中使用ORC和Parquet兩種列式存儲格式執行TPC-DS基準測試的結果,測試結果能夠看出在數據存儲方面,兩種存儲格式在都是用snappy壓縮的狀況下量中存儲格式佔用的空間相差並不大,查詢的結果顯示Parquet格式稍好於ORC格式,二者在功能上也都有優缺點,Parquet原生支持嵌套式數據結構,而ORC對此支持的較差,這種複雜的Schema查詢也相對較差;而Parquet不支持數據的修改和ACID,可是ORC對此提供支持,可是在OLAP環境下不多會對單條數據修改,更多的則是批量導入。
自從2012年由Twitter和Cloudera共同研發Parquet開始,該項目一直處於高速發展之中,而且在項目之初就將其貢獻給開源社區,2013年,Criteo公司加入開發而且向Hive社區提交了向hive集成Parquet的patch(HIVE-5783),在Hive 0.13版本以後正式加入了Parquet的支持;以後愈來愈多的查詢引擎對此進行支持,也進一步帶動了Parquet的發展。
目前Parquet正處於向2.0版本邁進的階段,在新的版本中實現了新的Page存儲格式,針對不一樣的類型優化編碼算法,另外豐富了支持的原始類型,增長了Decimal、Timestamp等類型的支持,增長更加豐富的統計信息,例如Bloon Filter,可以儘量得將謂詞下推在元數據層完成。
本文介紹了一種支持嵌套數據模型對的列式存儲系統Parquet,做爲大數據系統中OLAP查詢的優化方案,它已經被多種查詢引擎原生支持,而且部分高性能引擎將其做爲默認的文件存儲格式。經過數據編碼和壓縮,以及映射下推和謂詞下推功能,Parquet的性能也較之其它文件格式有所提高,能夠預見,隨着數據模型的豐富和Ad hoc查詢的需求,Parquet將會被更普遍的使用。