Spark Parquet詳解

Spark - Parquet

概述

Apache Parquet屬於Hadoop生態圈的一種新型列式存儲格式,既然屬於Hadoop生態圈,所以也兼容大多圈內計算框架(Hadoop、Spark),另外Parquet是平臺、語言無關的,這使得它的適用性很廣,只要相關語言有對應支持的類庫就能夠用;python

Parquet的優劣對比:算法

  • 支持嵌套結構,這點對比一樣是列式存儲的OCR具有必定優點;
  • 適用於OLAP場景,對比CSV等行式存儲結構,列示存儲支持映射下推謂詞下推,減小磁盤IO;
  • 一樣的壓縮方式下,列式存儲由於每一列都是同構的,所以可使用更高效的壓縮方法;

下面主要介紹Parquet如何實現自身的相關優點,毫不僅僅是使用了列式存儲就完了,而是在數據模型、存儲格式、架構設計等方面都有突破;sql

列式存儲 vs 行式存儲

區別在於數據在內存中是以行爲順序存儲仍是列爲順序,首先沒有哪一種方式更優,主要考慮實際業務場景下的數據量、經常使用操做等;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

在統計信息存放位置上,因爲統計信息一般是針對某一列的,所以列式存儲直接放到對應列的最後方或者最前方便可,行式存儲須要單獨存放

針對統計信息的耗時主要體如今數據插入刪除時的維護更新上:

  • 行式存儲:插入刪除每條數據都須要將年齡與最大最小值進行比較並判斷是否須要更新,若是是插入數據,那麼更新只須要分別於最大最小進行對比便可,若是是刪除數據,那麼若是刪除的偏偏是最大最小值,就還須要從現有數據中遍歷查找最大最小值來,這就須要遍歷全部數據;
  • 列式存儲:插入有統計信息的對應列時才須要進行比較,此處若是是插入姓名列,那就沒有比較的必要,只有年齡列會進行此操做,一樣對於年齡列進行刪除操做後的更新時,只須要針對該列進行遍歷便可,這在數據維度很大的狀況下能夠縮小N(N爲數據列數)倍的查詢範圍;

數據架構

這部分主要分析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對嵌套的支持:

  • Student做爲整個schema的頂點,也是結構樹的根節點,由message關鍵字標識;
  • name做爲必須有一個值的列,用required標識,類型爲string
  • age做爲可選項,能夠有一個值也能夠沒有,用optinal標識,類型爲string
  • score做爲必須有一個值的列,用required標識,類型爲double
  • hobbies做爲能夠沒有也能夠有多個的列,用repeated標識,類型爲group,也就是嵌套類型;
    • hobby_name屬於hobbies中元素的屬性,必須有一個,類型爲string;
    • home_page屬於hobbies中元素的屬性,能夠有一個也能夠沒有,類型爲string;

能夠看到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 & Repeatition level

解決上述歧義問題是經過定義等級重複等級來完成的,下面依次介紹這兩個比較難以直觀理解的概念;

Definition level 定義等級

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)

Repetition level 重複等級

針對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的文件格式主要由headerfooter、Row group、Column、Page組成,這種形式也是爲了支持在hadoop等分佈式大數據框架下的數據存儲,所以這部分看起來總讓人想起hadoop的分區。。。。。。

結合下面的官方格式展現圖:

能夠看到圖中分爲左右兩部分:

  • 左邊:
    • 最外層表示一個Parquet文件;
    • 首先是Magic Number,用於校驗Parquet文件,而且也能夠用於表示文件開始和結束位;
    • 一個File對應個Row group;
    • 一個Row group對應個Column;
    • 一個Column對應個Page;
    • Page是最小邏輯存儲單元,其中包含頭信息重複等級定義等級以及對應的數據值
  • 右邊:
    • Footer中包含重要的元數據;
    • 文件元數據包含版本架構額外的k/v對等;
    • Row group元數據包括其下屬各個Column的元數據;
    • Column的元數據包含數據類型路徑編碼偏移量壓縮/未壓縮大小額外的k/v對等;

文件格式的設定一方面是針對Hadoop等分佈式結構的適應,另外一方面也是對其嵌套支持高效壓縮等特性的支持,因此以爲從這方面理解會更容易一些,好比:

  • 嵌套支持:從上一章節知道列式存儲支持嵌套中Repetition levelDefinition level是很重要的,這兩者都存放於Row group的元數據中;
  • 高效壓縮:注意到每一個Column都有一個type元數據,那麼壓縮算法能夠經過這個屬性來進行對應壓縮,另外元數據中的額外k/v對能夠用於存放對應列的統計信息

Python導入導出Parquet格式文件

最後給出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就直接讀取就好,畢竟都是一家人。。。。

參考

文中的不少概念、例子等都來自於下面兩篇分享,須要的同窗能夠移步那邊;

相關文章
相關標籤/搜索