Apache Pig的一些基礎概念及用法總結(轉)

Apache pig是用來處理大規模數據的高級查詢語言,配合Hadoop使用,可以在處理海量數據時達到事半功倍的效果,比使用Java,C++等語言編寫大規模數據處理程序的難度要小N倍,實現同樣的效果的代碼量也小N倍。Twitter就大量使用pig來處理海量數據——有興趣的,可以看Twitter工程師寫的這個PPT
但是,剛接觸pig時,可能會覺得裏面的某些概念以及程序實現方法與想像中的很不一樣,甚至有些莫名,所以,你需要仔細地研究一下基礎概念,這樣在寫pig程序的時候,纔不會覺得非常彆扭。

本文基於以下環境:
pig 0.8.1

先給出兩個鏈接:pig參考手冊1pig參考手冊2。本文的部分內容來自這兩個手冊,但涉及到翻譯的部分,也是我自己翻譯的,因此可能理解與英文有偏差,如果你覺得有疑義,可參考英文內容。

(1)關係(relation)、包(bag)、元組(tuple)、字段(field)、數據(data)的關係

  • 一個關係(relation)是一個包(bag),更具體地說,是一個外部的包(outer bag)。
  • 一個包(bag)是一個元組(tuple)的集合。在pig中表示數據時,用大括號{}括起來的東西表示一個包——無論是在教程中的實例演示,還是在pig交互模式下的輸出,都遵循這樣的約定,請牢記這一點,因爲不理解的話就會對數據結構的掌握產生偏差
  • 一個元組(tuple)是若干字段(field)的一個有序集(ordered set)。在pig中表示數據時,用小括號()括起來的東西表示一個元組
  • 一個字段是一塊數據(data)。

“元組”這個詞很抽象,你可以把它想像成關係型數據庫表中的一行,它含有一個或多個字段,其中,每一個字段可以是任何數據類型,並且可以有或者沒有數據。
“關係”可以比喻成關係型數據庫的一張表,而上面說了,“元組”可以比喻成數據表中的一行,那麼這裏有人要問了,在關係型數據庫中,同一張表中的每一行都有固定的字段數,pig中的“關係”與“元組”之間,是否也是這樣的情況呢?不是的。“關係”並不要求每一個“元組”都含有相同數量的字段,並且也不會要求各“元組”中在相同位置處的字段具有相同的數據類型(太隨意了,是吧?)
文章來源:http://www.codelast.com/
(2)一個 計算多維度組合下的平均值 的實際例子
爲了幫助大家理解pig的一個基本的數據處理流程,我造了一些簡單的數據來舉個例子——
假設有數據文件:a.txt(各數值之間是以tab分隔的):

1
2
3
4
5
6
7
[root@localhost pig]$ cat a.txt
a 1 2 3 4.2 9.8
a 3 0 5 3.5 2.1
b 7 9 9 - -
a 7 9 9 2.6 6.2
a 1 2 5 7.7 5.9
a 1 2 3 1.4 0.2

問題如下:怎樣求出在第2、3、4列的所有組合的情況下,最後兩列的平均值分別是多少?
例如,第2、3、4列有一個組合爲(1,2,3),即第一行和最後一行數據。對這個維度組合來說,最後兩列的平均值分別爲:
(4.2+1.4)/2=2.8
(9.8+0.2)/2=5.0
而對於第2、3、4列的其他所有維度組合,都分別只有一行數據,因此最後兩列的平均值其實就是它們自身。
特別地,組合(7,9,9)有兩行記錄:第三、四行,但是第三行數據的最後兩列沒有值,因此它不應該被用於平均值的計算,也就是說,在計算平均值時,第三行是無效數據。所以(7,9,9)組合的最後兩列的平均值爲 2.6 和 6.2。
我們現在用pig來算一下,並且輸出最終的結果。
先進入本地調試模式(pig -x local),再依次輸入如下pig代碼:

1
2
3
4
A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);
B = GROUP A BY (col2, col3, col4);
C = FOREACH B GENERATE group, AVG(A.col5), AVG(A.col6);
DUMP C;

pig輸出結果如下:

1
2
3
4
((1,2,3),2.8,5.0)
((1,2,5),7.7,5.9)
((3,0,5),3.5,2.1)
((7,9,9),2.6,6.2)

這個結果對嗎?手工算一下就知道是對的。
文章來源:http://www.codelast.com/
下面,我們依次來看看每一句pig代碼分別得到了什麼樣的數據。
加載 a.txt 文件,並指定每一列的數據類型分別爲 chararray(字符串),int,int,int,double,double。同時,我們還給予了每一列別名,分別爲 col1,col2,……,col6。這個別名在後面的數據處理中會用到——如果你不指定別名,那麼在後面的處理中,就只能使用索引($0,$1,……)來標識相應的列了,這樣可讀性會變差,因此,在列固定的情況下,還是指定別名的好。
將數據加載之後,保存到變量A中,A的數據結構如下:

1
A: {col1: chararray,col2: int,col3: int,col4: int,col5: double,col6: double}

可見,A是用大括號括起來的東西。根據本文前面的說法,A是一個包(bag)。
這個時候,A與你想像中的樣子應該是一致的,也就是與前面打印出來的 a.txt 文件的內容是一樣的,還是一行一行的類似於“二維表”的數據。
文章來源:http://www.codelast.com/
按照A的第2、3、4列,對A進行分組。pig會找出所有第2、3、4列的組合,並按照升序進行排列,然後將它們與對應的包A整合起來,得到如下的數據結構:

1
B: {group: (col2: int,col3: int,col4: int),A: {col1: chararray,col2: int,col3: int,col4: int,col5: double,col6: double}}

可見,A的第2、3、4列的組合被pig賦予了一個別名:group,這很形象。同時我們也觀察到,B的每一行其實就是由一個group和若干個A組成的——注意,是若干個A。這裏之所以只顯示了一個A,是因爲這裏表示的是數據結構,而不表示具體數據有多少組。
實際的數據爲:

1
2
3
4
((1,2,3),{(a,1,2,3,4.2,9.8),(a,1,2,3,1.4,0.2)})
((1,2,5),{(a,1,2,5,7.7,5.9)})
((3,0,5),{(a,3,0,5,3.5,2.1)})
((7,9,9),{(b,7,9,9,,),(a,7,9,9,2.6,6.2)})

可見,與前面所說的一樣,組合(1,2,3)對應了兩行數據,組合(7,9,9)也對應了兩行數據。
這個時候,B的結構就不那麼明朗了,可能與你想像中有一點不一樣了。
文章來源:http://www.codelast.com/
計算每一種組合下的最後兩列的平均值。
根據上面得到的B的數據,你可以把B想像成一行一行的數據(只不過這些行不是對稱的),FOREACH 的作用是對 B 的每一行數據進行遍歷,然後進行計算。
GENERATE 可以理解爲要生成什麼樣的數據,這裏的 group 就是上一步操作中B的第一項數據(即pig爲A的第2、3、4列的組合賦予的別名),所以它告訴了我們:在數據集 C 的每一行裏,第一項就是B中的group——類似於(1,2,5)這樣的東西)。
而 AVG(A.col5) 這樣的計算,則是調用了pig的一個求平均值的函數 AVG,用於對 A 的名爲 col5 的列求平均值。前文說了,在加載數據到A的時候,我們已經給每一列起了個別名,col5就是倒數第二列。
到這裏,可能有人要迷糊了:難道 AVG(A.col5) 不是表示對 A 的col5這一列求平均值嗎?也就是說,在遍歷B(FOREACH B)的每一行時候,計算結果都是相同的啊!
事實上並不是這樣。我們遍歷的是B,我們需要注意到,B的數據結構中,每一行數據裏,一個group對應的是若干個A,因此,這裏的 A.col5,指的是B的每一行中的A,而不是包含全部數據的那個A。拿B的第一行來舉例:
((1,2,3),{(a,1,2,3,4.2,9.8),(a,1,2,3,1.4,0.2)})
遍歷到B的這一行時,要計算AVG(A.col5),pig會找到 (a,1,2,3,4.2,9.8) 中的4.2,以及(a,1,2,3,1.4,0.2)中的1.4,加起來除以2,就得到了平均值。
同理,我們也知道了AVG(A.col6)是怎麼算出來的。但還有一點要注意的:對(7,9,9)這個組,它對應的數據(b,7,9,9,,)裏最後兩列是無值的,這是因爲我們的數據文件對應位置上不是有效數字,而是兩個“-”,pig在加載數據的時候自動將它置爲空了,並且計算平均值的時候,也不會把這一組數據考慮在內(相當於忽略這組數據的存在)。
到了這裏,我們不難理解,爲什麼C的數據結構是這樣的了:

1
C: {group: (col2: int,col3: int,col4: int),double,double}

文章來源:http://www.codelast.com/
DUMP C就是將C中的數據輸出到控制檯。如果要輸出到文件,需要使用:

1
STORE C INTO 'output';

這樣pig就會在當前目錄下新建一個“output”目錄(該目錄必須事先不存在),並把結果文件放到該目錄下。

請想像一下,如果要實現相同的功能,用Java或C++寫一個Map-Reduce應用程序需要多少時間?可能僅僅是寫一個build.xml或者Makefile,所需的時間就是寫這段pig代碼的幾十倍了!
正因爲pig有如此優勢,它纔得到了廣泛應用。
文章來源:http://www.codelast.com/
(3)怎樣統計數據行數
在SQL語句中,要統計表中數據的行數,很簡單:

1
SELECT COUNT(*) FROM table_name WHERE condition

在pig中,也有一個COUNT函數,在pig手冊中,對COUNT函數有這樣的說明:

Computes the number of elements in a bag.

假設要計算數據文件a.txt的行數:

1
2
3
4
5
6
7
[root@localhost pig]$ cat a.txt
a 1 2 3 4.2 9.8
a 3 0 5 3.5 2.1
b 7 9 9 - -
a 7 9 9 2.6 6.2
a 1 2 5 7.7 5.9
a 1 2 3 1.4 0.2

你是否可以這樣做呢:

1
2
3
A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);
B = COUNT(*);
DUMP B;

答案是:絕對不行。pig會報錯。pig手冊中寫得很明白:

Note: You cannot use the tuple designator (*) with COUNT; that is, COUNT(*) will not work.

那麼,這樣對某一列計數行不行呢:

1
B = COUNT(A.col2);

答案是:仍然不行。pig會報錯。
這就與我們想像中的“正確做法”有點不一樣了:我爲什麼不能直接統計一個字段的數目有多少呢?剛接觸pig的時候,一定非常疑惑這樣明顯“不應該出錯”的寫法爲什麼行不通。
要統計A中含col2字段的數據有多少行,正確的做法是:

1
2
3
4
A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);
B = GROUP A ALL;
C = FOREACH B GENERATE COUNT(A.col2);
DUMP C;

輸出結果:

1
(6)

表明有6行數據。
如此麻煩?沒錯。這是由pig的數據結構決定的。
文章來源:http://www.codelast.com/
在這個例子中,統計COUNT(A.col2)和COUNT(A)的結果是一樣的,但是,如果col2這一列中含有空值:

1
2
3
4
5
6
7
[root@localhost pig]$ cat a.txt
a 1 2 3 4.2 9.8
a   0 5 3.5 2.1
b 7 9 9 - -
a 7 9 9 2.6 6.2
a 1 2 5 7.7 5.9
a 1 2 3 1.4 0.2

則以下pig程序及執行結果爲:

1
2
3
4
5
grunt> A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);
grunt> B = GROUP A ALL;
grunt> C = FOREACH B GENERATE COUNT(A.col2);
grunt> DUMP C;
(5)

可見,結果爲5行。那是因爲你LOAD數據的時候指定了col2的數據類型爲int,而a.txt的第二行數據是空的,因此數據加載到A以後,有一個字段就是空的:

1
2
3
4
5
6
7
grunt> DUMP A;
(a,1,2,3,4.2,9.8)
(a,,0,5,3.5,2.1)
(b,7,9,9,,)
(a,7,9,9,2.6,6.2)
(a,1,2,5,7.7,5.9)
(a,1,2,3,1.4,0.2)

在COUNT的時候,null的字段不會被計入在內,所以結果是5。

The COUNT function follows syntax semantics and ignores nulls. What this means is that a tuple in the bag will not be counted if the first field in this tuple is NULL. If you want to include NULL values in the count computation, use COUNT_STAR.

文章來源:http://www.codelast.com/

(4)FLATTEN操作符的作用
這個玩意一開始還是挺讓我費解的。從字面上看,flatten就是“弄平”的意思,但是在對一個pig的數據結構操作時,flatten到底是“弄平”了什麼,又有什麼作用呢?
我們還是採用前面的a.txt數據文件來說明:
1
2
3
4
5
6
7
[root@localhost pig]$ cat a.txt
a 1 2 3 4.2 9.8
a 3 0 5 3.5 2.1
b 7 9 9 - -
a 7 9 9 2.6 6.2
a 1 2 5 7.7 5.9
a 1 2 3 1.4 0.2

如果我們按照前文的做法,計算多維度組合下的最後兩列的平均值,則:

1
2
3
4
5
6
7
8
grunt> A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);
grunt> B = GROUP A BY (col2, col3, col4);
grunt> C = FOREACH B GENERATE group, AVG(A.col5), AVG(A.col6);
grunt> DUMP C;
((1,2,3),2.8,5.0)
((1,2,5),7.7,5.9)
((3,0,5),3.5,2.1)
((7,9,9),2.6,6.2)

可見,輸出結果中,每一行的第一項是一個tuple(元組),我們來試試看 FLATTEN 的作用:

1
2
3
4
5
6
7
8
grunt> A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);
grunt> B = GROUP A BY (col2, col3, col4);
grunt> C = FOREACH B GENERATE FLATTEN(group), AVG(A.col5), AVG(A.col6);
grunt> DUMP C;
(1,2,3,2.8,5.0)
(1,2,5,7.7,5.9)
(3,0,5,3.5,2.1)
(7,9,9,2.6,6.2)

看到了嗎?被 FLATTEN 的group本來是一個元組,現在變成了扁平的結構了。按照pig文檔的說法,FLATTEN用於對元組(tuple)和包(bag)“解嵌套”(un-nest):

The FLATTEN operator looks like a UDF syntactically, but it is actually an operator that changes the structure of tuples and bags in a way that a UDF cannot. Flatten un-nests tuples as well as bags. The idea is the same, but the operation and result is different for each type of structure.
 
For tuples, flatten substitutes the fields of a tuple in place of the tuple. For example, consider a relation that has a tuple of the form (a, (b, c)). The expression GENERATE $0, flatten($1), will cause that tuple to become (a, b, c).

文章來源:http://www.codelast.com/
所以我們就看到了上面的結果。
在有的時候,不“解嵌套”的數據結構是不利於觀察的,輸出這樣的數據可能不利於外圍數程序的處理(例如,pig將數據輸出到磁盤後,我們還需要用其他程序做後續處理,而對一個元組,輸出的內容裏是含括號的,這就在處理流程上又要多一道去括號的工序),因此,FLATTEN提供了一個讓我們在某些情況下可以清楚、方便地分析數據的機會。

(5)關於GROUP操作符
在上文的例子中,已經演示了GROUP操作符會生成什麼樣的數據。在這裏,需要說得更理論一些:

  • 用於GROUP的key如果多於一個字段(正如本文前面的例子),則GROUP之後的數據的key是一個元組(tuple),否則它就是與用於GROUP的key相同類型的東西。
  • GROUP的結果是一個關係(relation),在這個關係中,每一組包含一個元組(tuple),這個元組包含兩個字段:(1)第一個字段被命名爲“group”——這一點非常容易與GROUP關鍵字相混淆,但請區分開來。該字段的類型與用於GROUP的key類型相同。(2)第二個字段是一個包(bag),它的類型與被GROUP的關係的類型相同。

(6)把數據當作“元組”(tuple)來加載
還是假設有如下數據:

1
2
3
4
5
6
7
[root@localhost pig]$ cat a.txt
a 1 2 3 4.2 9.8
a 3 0 5 3.5 2.1
b 7 9 9 - -
a 7 9 9 2.6 6.2
a 1 2 5 7.7 5.9
a 1 2 3 1.4 0.2

如果我們按照以下方式來加載數據:

1
A = LOAD 'a.txt' AS (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double);

那麼得到的A的數據結構爲:

1
2
grunt> DESCRIBE A;
A: {col1: chararray,col2: int,col3: int,col4: int,col5: double,col6: double}

如果你要把A當作一個元組(tuple)來加載:

1
A = LOAD 'a.txt' AS (T : tuple (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double));

也就是想要得到這樣的數據結構:

1
2
grunt> DESCRIBE A;
A: {T: (col1: chararray,col2: int,col3: int,col4: int,col5: double,col6: double)}

那麼,上面的方法將得到一個空的A:

1
2
3
4
5
6
7
grunt> DUMP A;
()
()
()
()
()
()

那是因爲數據文件a.txt的結構不適合於這樣加載成元組(tuple)。
文章來源:http://www.codelast.com/
如果有數據文件b.txt:

1
2
3
4
5
6
7
[root@localhost pig]$ cat b.txt
(a,1,2,3,4.2,9.8)
(a,3,0,5,3.5,2.1)
(b,7,9,9,-,-)
(a,7,9,9,2.6,6.2)
(a,1,2,5,7.7,5.9)
(a,1,2,3,1.4,0.2)

則使用上面所說的加載方法及結果爲:

1
2
3
4
5
6
7
8
grunt> A = LOAD 'b.txt' AS (T : tuple (col1:chararray, col2:int, col3:int, col4:int, col5:double, col6:double));
grunt> DUMP A;
((a,1,2,3,4.2,9.8))
((a,3,0,5,3.5,2.1))
((b,7,9,9,,))
((a,7,9,9,2.6,6.2))
((a,1,2,5,7.7,5.9))
((a,1,2,3,1.4,0.2))

可見,加載的數據的結構確實被定義成了元組(tuple)。

轉載:http://www.codelast.com/



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