《Apache Drill學習筆記一:環境搭建和簡單試用》提到過Apache Drill是受Google的Dremel系統啓發而設計實現的,這出於Google公開於2010年的論文「Dremel Interactive Analysis of WebScaleDatasets」。爲了弄清楚Apache Drill的運行機制,這篇論文是必定要先仔細研讀的,不然就只能像我以前那樣僅僅將其做爲CSV或者JSON的SQL查詢工具使用了,而不能真正發揮其強大的性能優點。數據庫
簡單說Dremel是Google的「交互式」數據分析系統,能夠組建成規模上千的集羣,處理PB級別的數據。雖然MapReduce也能夠處理這樣規模的數據,但它所須要的時間相對比較長,適合數據的批處理,而不適合交互式查詢的場景,Dremel正是這樣的一個有力補充。數組
Dremel有2個顯著特色:ruby
而這正是其餘數據庫、查詢引擎的痛點所在,也正是咱們須要着重瞭解的地方。微信
Dremel使用的數據就是咱們熟悉的Protocol Buffer格式,但一般狀況咱們都是做爲序列化方法或者在RPC中傳輸等場景使用,較少用它來存放大量數據。對於沒有接觸過Protocol Buffer的讀者,能夠用JSON類比,兩者結構很類似,一個不一樣是Protocol Buffer不支持JSON的map(或者說是dict、hashmap)。數據結構
一個Protocol Buffer的Document.proto
文件示例:dom
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。但從中已經能夠看出2個特色:工具
對如此複雜的數據作SQL查詢看起來是很讓人頭疼的,咱們天然想到先簡化一下,從最簡單的狀況考慮。性能
這種數據格式用數學方法嚴格表示是這樣的:學習
t = dom | <A1:t[*|?], ..., An:t[*|?]>
看起來有點複雜,但理解起來很容易。t(原文是希臘字母τ,但爲了書寫方便這裏改爲英文字母t)是一個數據類型的定義,而.proto文件就是定義一個或多個數據類型。t有兩種可能(|和c語言同樣是「或」的意思,一種是基本類型dom(如int、string、float等),另外一種是使用遞歸方式定義的,即t能夠由其餘以前定義好的t組成,就像c中的結構體同樣,與結構體不大相同的是,每一個包含的t的值能夠有多個(*,repeated,相似c中的數組),還能夠是可選的(?,optional,以前那個數組能夠不包含任何元素)。A1-An是這些t的命名(也就是A1是某個t類型的變量)。其實從這個定義中更容易看出以前總結的2個特色。優化
如今咱們來考慮簡單的Protocol Buffer數據,以及如何查詢。
這是一個簡化的Document.proto
,能夠看到它只有一層結構,並且沒有repeated
和optional
字段。
message Document { required int64 DocId; string Url; string Country; int64 Code; }
而Document
的數據就是一張普通的二維表:
DocId | Url | Country | Code |
---|---|---|---|
10001 | http://1 | America | 10 |
10002 | http://2 | America | 20 |
10003 | http://3 | China | 30 |
10004 | http://4 | America | 40 |
10005 | http://5 | Japan | 50 |
10006 | http://6 | America | 60 |
... | ... | ... | ... |
能夠看出咱們用二維的方式組織數據,但實際是數據在磁盤的地址是一維的,也就是咱們須要按某種方式把它拼接成一維的數據。那最基本的方式有兩種:
10001 | http://1 | America | 10 | 10002 | http://2 | America | 20 | ...
-> | -> | -> | -> | -> | -> | -> | -> | -> |
10001 | 10002 | 1003 | ... | http://1 | http://2 | http://3 | ...
-> | -> | -> | -> | -> | -> | -> | -> |
咱們先考慮下對這個表進行select
,如select Url, Code from Document;
若是是按行存的話,每讀一個Url
後,都須要跳到下一個Url
的位置,全部要查出的字段都不是連續存放的。並且由於有字符串這樣的非定長字段(若是使用定長的預留空間,又會形成大量的空間浪費),不能經過簡單計算就能夠獲得地址,查起來很是痛苦,效率天然不會很高。
而按列存的狀況就好不少,只須要找到第一個Url
和第一個Code
的首地址,而後順序讀取到結尾便可。不只實現簡單,並且磁盤順序讀取比如隨機讀取要快,加上更容易優化(好比把臨近地址的數據預讀到內存,連續的同類型數據更容易壓縮存放),效率天然不可同日而語。
那是否是全部狀況都須要按列來存數據呢?顯然不是。雖然按列讀的狀況比較多,但寫入通常是按行寫的,不管是追加、刪除、修改,通常都是按行處理的。數據按列存的話,追加時須要把一行數據按字段拆開,分別插入到不一樣的地方,刪除也是同樣,修改更加痛苦。由於若是是相似字符串的不定長字段,按行存的話能夠以行爲單位預留空間,而按列存的話須要以字段爲單位預留空間,或者使用更復雜的方法。想想就要麻煩許多。
數據庫每每須要同時照顧到讀和寫的效率,簡單的按行存或者按列存都存在明顯的問題(包括下文提到的表join效率等問題),因此每每須要存儲複雜的meta數據、添加各種索引、使用各類樹型甚至圖型結構,來在讀和寫之間謀得一個平衡點。
而Dremel要輕鬆一些,由於它被設計成一個查詢引擎,即便也有寫入功能也不會過多考慮寫入的效率,那麼顯然按列存是合適的。這樣即便一張表字段不少,數據量很大,只要記錄每一個字段的類型以及對應數據的起始地址等少許信息,查起來就遊刃有餘。因此若是隻是用來查一個巨大的二維表的後,並非很難。
但咱們知道,平時使用的數據很難在一張二維表裏表達清楚,每每須要多張表,互相還有關聯,查詢起來就須要各類join。數據量小還好,數據量一大,join效率直線降低,單表select再快也沒用,這纔是真正棘手的問題。
Dremel的解決方法不是設法提升join的效率,而是換一種思路,使用嵌套的數據解決簡單二維表表達能力太弱的缺點。
再拿出以前的Document.proto
:
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; } }
這樣的數據若是用二維表來存放通常須要多張才能描述清楚,處理重複字段也比較痛苦,而一個Protocol Buffer類型就能夠描述,但在磁盤的實際存放仍是要動很多腦筋的。
如今就須要搬出論文裏的這張圖了:
雖然嵌套的數據比以前的二維表更加複雜,仍是有按行存和按列存兩種基本方法,並且正如咱們以前提到的,爲了查詢效率,咱們採用按列存的方法(圖中的column-oriented
)。咱們重點關注A、B、C、D、E這些樹型關係如何存儲。
咱們來準備一些符合Document.proto
的簡單的數據:
DocId: 10 Links Forward: 20 Forward: 40 Forward: 60 Name Language Code: 'en-us' Country: 'us' Language Code: 'en' Url: 'http://A' Name Url: 'http://B' Name Language Code: 'en-gb' Country: 'gb'
DocId: 20 Links Backward: 10 Backward: 30 Forward: 80 Name Url: 'http://C'
其中DocId: 10
和DocId: 20
是兩個Document
。
Dremel是這樣拆解數據的:
能夠看出每一個須要存放實際數據的葉子節點都變成了一張二維表,但表中除了字段自身的值。若是是repeated
字段,則在表中增添行;若是是optional
字段,而且數據中不填充,則用NULL
代替(而不是去掉這一行)。但還出現了r
和d
,這兩個又是什麼東西,並且爲什麼要記錄NULL
呢?
試想若是去掉上圖中r
和d
兩列,則每一個二維表都變成了一個一維表(list),那麼咱們試圖把數據還原回去,DocId
沒問題,必定是屬於兩個Document
的。Name.Url
就出現了問題,由於Name
是repeated
的,我怎麼知道這3個Name.Url
是全屬於第一個Document
,仍是其餘狀況呢?丟失的信息太多沒法還原了。全部咱們須要記錄每一個值是不是重複的以及在哪一層重複的(好比是在第一個Name
的第二個Code
,仍是第二個Name
的第一個Code
)。有了這個信息,咱們就能夠根據以前的記錄一個一個往上拼接來還原原始的數據結構。r
就是作這個的。
r
是重複層次(Repetition Level),記錄該列的值是在哪個層次上重。
若是r
是0,則表示是第一個(非重複)的元素,如上圖中的DocId
,兩個DocId都是第一個元素,比較簡單。但其餘的字段就比較複雜了,如Name.Language.Code
,一共有五行:
en-us
是第一個Document
(不一樣的Document
不算重複,不影響r
和d
的取值,只有repeated
類型的字段纔算)裏第一個Name
中的第一個Language
裏的,重複尚未發生,因此r
是0。en
是第一個Document
裏第一個Name
中第二個Language
裏的,Language
發生了重複,在/Name/Language層次結構中處於第二層,因此r
是2。en-gb
是第一個Document
裏第三個Name
中第一個Language
裏的,Name
發生了重複,在/Name/Language層次結構中處於第一層,因此r
是1。NULL
是第一個Document
裏第二個Name
中的,Name
發生了重複,在/Name/Language層次結構中處於第一層,因此r
是1。NULL
是第二個Document
裏第一個Name
中的,沒有發生重複,因此r
是0。這裏例子中沒有出現多個字段都發生重複的狀況,如第二個Name
中的第二個Language
的Code
。若是是這種狀況,那麼r
取最大的,也就是最近發生重複的字段,這裏例子中就是Language
的2。(待驗證)
以前還有個問題沒有回答,爲什麼要記錄NULL
呢?
若是把圖中全部的NULL
都去掉,看會發生什麼。 拿Links.Backward
舉例,去掉第一行的NULL
後,咱們讀到第一個Links.Backward
,必然認爲它是屬於第一個Document
的,但實際數據中第一個Document
裏沒有Links.Backward
,徹底搞錯了。因此即便是NULL
也必須記錄,爲了後續的數據知道本身在哪。
那麼有了r
後,是否信息就完善了呢?
咱們仍是假設去掉d
的一列,試圖還原數據。DocId
依然沒問題,Name.Url
也沒問題了,直接看Name.Language.Country
吧:
讀完第一行咱們獲得了:
Document Name Language Country : 'us'
第二行是個NULL
,是在第二層也就是Language
重複的:
Document Name Language Country : 'us' Language Country : NULL
第三行又是個NULL
,是在第一層也就是Name
重複的:
Document Name Language Country : 'us' Language Country : NULL Name Language Country : NULL
第四行是在第一層也就是Name
重複的:
Document Name Language Country : 'us' Language Country : NULL Name Language Country : NULL Name Language Country : 'gb'
看起來彷佛沒問題,不過對比原始數據發現第二個Name
不僅沒有Country
,連上層的Language
也沒有。也就是單看Name.Language.Country
這個表,仍是把數據還原錯了。雖然把全部的表都還原出來,而後去掉全部的NULL
以及NULL
上邊多餘的部分,仍是能夠準確還原,但若是隻是去查詢某個字段,難道須要把其餘全部字段所有分析一遍嗎?另外沒有發生重複的字段,具體是required
、repeated
、仍是optional
的信息也丟了。(此處彷佛還有其餘問題)
爲了解決這個問題,d
被引入了。
d
是定義層次(Definition Level),記錄這個值是在哪一層被定義的。須要注意的是若是這個值是required
的,則層數不包括自身,不然若是是repeated
或optional
的,則包括自身。目的主要是區分是不是required
字段(但如何區分只有一行的repeated
和optional
呢?)。
舉例:
Document.Links.Backward
的d
是2(Document
是0)Document.Name.Language.Code
也是2(由於Code
是required
的,因此不包括它本身)對於通常的數據,這個值看起來沒什麼意義(除了能夠區分是不是required
字段),由於已經有值了,從根到它自身整條路徑必然是存在的,但對於NULL
則不一樣,d
能夠說明這個NULL
是在哪一層定義的,也就是解決咱們以前還原Name.Language.Country
數據遇到的問題。
r
和d
這兩個值仍是須要好好理解一下,並且還有一些沒弄清楚的細節,以及具體查詢的複雜邏輯,只能後續繼續學習了。
付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活訂價,歡迎諮詢,微信 ly50247。