Apache Parquet屬於Hadoop生態圈的一種新型列式存儲格式,既然屬於Hadoop生態圈,所以也兼容大多圈內計算框架(Hadoop、Spark),另外Parquet是平臺、語言無關的,這使得它的適用性很廣,只要相關語言有對應支持的類庫就能夠用;python
Parquet的優劣對比:算法
下面主要介紹Parquet如何實現自身的相關優點,毫不僅僅是使用了列式存儲就完了,而是在數據模型、存儲格式、架構設計等方面都有突破;sql
區別在於數據在內存中是以行爲順序存儲仍是列爲順序,首先沒有哪一種方式更優,主要考慮實際業務場景下的數據量、經常使用操做等;session
例如兩個學生對象分別在行式和列式下的存儲狀況,假設學生對象具有姓名-string、年齡-int、平均分-double等信息:架構
行式存儲:框架
姓名 | 年齡 | 平均分 | 姓名 | 年齡 | 平均分 |
---|---|---|---|---|---|
張三 | 15 | 82.5 | 李四 | 16 | 77.0 |
列式存儲:分佈式
姓名 | 姓名 | 年齡 | 年齡 | 平均分 | 平均分 |
---|---|---|---|---|---|
張三 | 李四 | 15 | 16 | 82.5 | 77.0 |
乍一看彷佛沒有什麼區別,事實上如何不進行壓縮的化,兩種存儲方式實際存儲的數據量都是一致的,那麼確實沒有區別,可是實際上如今經常使用的數據存儲方式都有進行不一樣程度的壓縮,下面咱們考慮靈活進行壓縮的狀況下兩者的差別:oop
行式存儲是按照行來劃分最小單元,也就是說壓縮對象是某一行的數據,此處就是針對(張3、1五、82.5)這個數據組進行壓縮,問題是該組中數據格式並不一致且佔用內存空間大小不一樣,也就無法進行特定的壓縮手段;大數據
列式存儲則不一樣,它的存儲單元是某一列數據,好比(張3、李四)或者(15,16),那麼就能夠針對某一列進行特定的壓縮,好比對於姓名列,假設咱們值到最長的姓名長度那麼就能夠針對性進行壓縮,一樣對於年齡列,通常最大不超過120,那麼就可使用tiny int來進行壓縮等等,此處利用的就是列式存儲的同構性;ui
注意:此處的壓縮指的不是相似gzip這種通用的壓縮手段,事實上任何一種格式均可以進行gzip壓縮,這裏討論的壓縮是在此以外可以進一步針對存儲數據應用更加高效的壓縮算法以減小IO操做;
與上述數據壓縮相似,謂詞下推也是列式存儲特有的優點之一,繼續使用上面的例子:
行式存儲:
姓名 | 年齡 | 平均分 | 姓名 | 年齡 | 平均分 |
---|---|---|---|---|---|
張三 | 15 | 82.5 | 李四 | 16 | 77.0 |
列式存儲:
姓名 | 姓名 | 年齡 | 年齡 | 平均分 | 平均分 |
---|---|---|---|---|---|
張三 | 李四 | 15 | 16 | 82.5 | 77.0 |
假設上述數據中每一個數據值佔用空間大小都是1,所以兩者在未壓縮下佔用都是6;
咱們有在大規模數據進行以下的查詢語句:
SELECT 姓名,年齡 FROM info WHERE 年齡>=16;
這是一個很常見的根據某個過濾條件查詢某個表中的某些列,下面咱們考慮該查詢分別在行式和列式存儲下的執行過程:
事實上謂詞下推的使用主要依賴於在大規模數據處理分析的場景中,針對數據中某些列作過濾、計算、查詢的狀況確實更多,這一點有相關經驗的同窗應該感觸不少,所以這裏只能說列式存儲更加適用於該場景;
這部分直接用例子來理解,仍是上面的例子都是有一點點改動,爲了支持一些頻繁的統計信息查詢,針對年齡列增長了最大和最小兩個統計信息,這樣若是用戶查詢年齡列的最大最小值就不須要計算,直接返回便可,存儲格式以下:
行式存儲:
姓名 | 年齡 | 平均分 | 姓名 | 年齡 | 平均分 | 年齡最大 | 年齡最小 |
---|---|---|---|---|---|---|---|
張三 | 15 | 82.5 | 李四 | 16 | 77.0 | 16 | 15 |
列式存儲:
姓名 | 姓名 | 年齡 | 年齡 | 年齡最大 | 年齡最小 | 平均分 | 平均分 |
---|---|---|---|---|---|---|---|
張三 | 李四 | 15 | 16 | 16 | 15 | 82.5 | 77.0 |
在統計信息存放位置上,因爲統計信息一般是針對某一列的,所以列式存儲直接放到對應列的最後方或者最前方便可,行式存儲須要單獨存放;
針對統計信息的耗時主要體如今數據插入刪除時的維護更新上:
這部分主要分析Parquet使用的數據模型,以及其如何對嵌套類型的支持(須要分析repetition level和definition level);
數據模型這部分主要分析的是列式存儲如何處理不一樣行不一樣列之間存儲上的歧義問題,假設上述例子中增長一個興趣列,該列對應行能夠沒有數據,也能夠有多個數據(也就是說對於張三和李四,能夠沒有任何興趣,也能夠有多個,這種狀況對於行式存儲不是問題,可是對於列式存儲存在一個數據對應關係的歧義問題),假設興趣列存儲以下:
興趣 | 興趣 |
---|---|
羽毛球 | 籃球 |
事實上咱們並不肯定羽毛球和籃球到底都是張三的、都是李四的、仍是二人一人一個,這是由興趣列的特殊性決定的,這在Parquet數據模型中稱這一列爲repeated的;
上述例子的數據格式用parquet來描述以下:
message Student{ required string name; optinal int age; required double score; repeated group hobbies{ required string hobby_name; repeated string home_page; } }
這裏將興趣列複雜了一些以展現parquet對嵌套的支持:
能夠看到Parquet的schema結構中沒有對於List、Map等類型的支持,事實上List經過repeated支持,而Map則是經過group類型支持,舉例說明:
經過repeated支持List: [15,16,18,14] ==> repeated int ages; 經過repeated+group支持List[Map]: {'name':'李四','age':15} ==> repeated group Peoples{ required string name; optinal int age; }
從schema樹結構到列存儲;
仍是上述例子,看下schema的樹形結構:
矩形表示是一個葉子節點,葉子節點都是基本類型,Group不是葉子,葉子節點中顏色最淺的是optinal,中間的是required,最深的是repeated;
首先上述結構對應的列式存儲總共有5列(等於葉子節點的數量):
Column | Type |
---|---|
Name | string |
Age | int |
Score | double |
hobbies.hobby_name | string |
hobbies.page_home | string |
解決上述歧義問題是經過定義等級和重複等級來完成的,下面依次介紹這兩個比較難以直觀理解的概念;
Definition level指的是截至當前位置爲止,從根節點一路到此的路徑上有多少可選的節點被定義了,由於是可選的,所以required類型不統計在內;
若是一個節點被定義了,那麼說明到達它的路徑上的全部節點都是被定義的,若是一個節點的定義等級等於這個節點處的最大定義等級,那麼說明它是有數據的,不然它的定義等級應該更小纔對;
一個簡單例子講解定義等級:
message ExampleDefinitionLevel{ optinal group a{ required group b{ optinal string c; } } }
Value | Definition level | 說明 |
---|---|---|
a:null | 0 | a往上只有根節點,所以它最大定義等級爲1,可是它爲null,因此它的定義等級爲0; |
a:{b:null} | 不可能 | b是required的,所以它不可能爲null; |
a:{b:{c:null}} | 1 | c處最大定義等級爲2,由於b是required的不參與統計,可是c爲null,因此它的定義等級爲1; |
a:{b:{c:"foo"}} | 2 | c有數據,所以它的定義等級就等於它的最大定義等級,即2; |
到此,定義等級的計算公式以下:當前樹深度 - 路徑上類型爲required的個數 - 1(若是自身爲null);
針對repeated類型field,若是一個field重複了,那麼它的重複等級等於根節點到達它的路徑上的repeated節點的個數;
注意:這個重複指的是同一個父節點下的同一類field出現多個,若是是不一樣父節點,那也是不算重複的;
一樣以簡單例子進行分析:
message ExampleRepetitionLevel{ repeated group a{ required group b{ repeated group c{ required string d; repeated string e; } } } }
Value | Repetition level | 說明 |
---|---|---|
a:null | 0 | 根本沒有重複這回事。。。。 |
a:a1 | 0 | 對於a1,雖然不是null,可是field目前只有一個a1,也沒有重複; |
a:a1 a:a2 |
1 | 對於a2,前面有個a1此時節點a重複出現了,它的重複等級爲1,由於它上面也沒有其餘repeated節點了; |
a1:{b:null} | 0 | 對於b,a1看不到a2,所以沒有重複; |
a1:{b:null} a2:{b:null} |
1 | 對於a2的b,a2在a1後面,因此算出現重複,b自身不重複且爲null; |
a1:{b:{c:c1}} a2:{b:{c:c2}} |
1 | 對於c2,雖然看着好像以前有個c1,可是因爲他們分屬不一樣的父節點,所以c沒有重複,可是對於a2與a1依然是重複的,因此重複等級爲1; |
a1:{b:{c:c1}} a1:{b:{c:c2}} |
2 | 對於c2,他們都是從a1到b,父節點都是b,那麼此時field c重複了,c路徑上還有一個a爲repeated,所以重複等級爲2; |
這裏可能仍是比較難以理解,下面經過以前的張三李四的例子,來更加真切的感覺下在這個例子上的定義等級和重複等級;
Schema以及數據內容以下:
message Student{ required string name; optinal int age; required double score; repeated group hobbies{ required string hobby_name; repeated string home_page; } } Student 1: Name 張三 Age 15 Score 70 hobbies hobby_name 籃球 page_home nba.com hobbies hobby_name 足球 Student 2: Name 李四 Score 75
name列最好理解,首先它是required的,因此既不符合定義等級,也不符合重複等級的要求,又是第一層的節點,所以所有都是0;
name | 定義等級 | 重複等級 |
---|---|---|
張三 | 0 | 0 |
李四 | 0 | 0 |
score列所處層級、類型與name列一致,也所有都是0,這裏就不列出來了;
age列一樣處於第一層,可是它是optinal的,所以知足定義等級的要求,只有張三有age,定義等級爲1,路徑上只有它本身知足,重複等級爲0;
age | 定義等級 | 重複等級 |
---|---|---|
15 | 1 | 0 |
hobby_name列處於hobbies group中,類型是required,籃球、足球定義等級都是1(自身爲required不歸入統計),父節點hobbies爲repeated,歸入統計,籃球重複等級爲0,此時張三的數據中還沒有出現過hobby_name或者hobbies,而足球的父節點hobbies重複了,而hobbies路徑上重複節點數爲1,所以它的重複等級爲1;
hobbies.hobby_name | 定義等級 | 重複等級 |
---|---|---|
籃球 | 1 | 0 |
足球 | 1 | 1 |
home_page列只在張三的第一個hobbies中有,首先重複等級爲0,這點與籃球是一個緣由,而定義等級爲2,由於它是repeated,路徑上它的父節點也是repeated的;
hobbies.home_page | 定義等級 | 重複等級 |
---|---|---|
nba.com | 2 | 0 |
到此對兩個雖然簡單,可是也包含了Parquet的三種類型、嵌套group等結構的例子進行了列式存儲分析,對此有個基本概念就行,其實就是兩個等級的定義問題;
Parquet的文件格式主要由header、footer、Row group、Column、Page組成,這種形式也是爲了支持在hadoop等分佈式大數據框架下的數據存儲,所以這部分看起來總讓人想起hadoop的分區。。。。。。
結合下面的官方格式展現圖:
能夠看到圖中分爲左右兩部分:
文件格式的設定一方面是針對Hadoop等分佈式結構的適應,另外一方面也是對其嵌套支持、高效壓縮等特性的支持,因此以爲從這方面理解會更容易一些,好比:
最後給出Python使用Pandas和pyspark兩種方式對Parquet文件的操做Demo吧,實際使用上因爲相關庫的封裝,對於調用者來講除了導入導出的API略有不一樣,其餘操做是徹底一致的;
Pandas:
import pandas as pd pd.read_parquet('parquet_file_path', engine='pyarrow')
上述代碼須要注意的是要單獨安裝pyarrow庫,不然會報錯,pandas是基於pyarrow對parquet進行支持的;
PS:這裏沒有安裝pyarrow,也沒有指定engine的話,報錯信息中說能夠安裝pyarrow或者fastparquet,可是我這裏試過fastparquet加載個人parquet文件會失敗,個人parquet是spark上直接導出的,不知道是否是兩個庫對parquet支持上有差別仍是由於啥,pyarrow就能夠。。。。
pyspark:
from pyspark import SparkContext from pyspark.sql.session import SparkSession ss = SparkSession(sc) ss.read.parquet('parquet_file_path') # 默認讀取的是hdfs的file
pyspark就直接讀取就好,畢竟都是一家人。。。。
文中的不少概念、例子等都來自於下面兩篇分享,須要的同窗能夠移步那邊;