Parquetは便利なファイル形式で、列志向のフォーマットとしてはデファクト の1つと言っても過言ではないでしょう。
ですが、json やcsv とは違い、ファイルを見ただけでどんな構造かわかるものではありません。
この記事は、Parquetの具体的な構造について記述します。
はじめに
この投稿は、Parquetの構造について、バイナリを見ながら確認するものです。
ただし、Parquetの大枠に注目した投稿なので、delta encodingやrun-lengthなど、個別の圧縮方法については取り扱いません。
※ Parquetの作成には https://github.com/parquet-go/parquet-go を使用していますが、goの知識は必要ありません
tldr
Parquetは以下の構造を持っています。
ファイルはRowGroupとメタデータ に分かれている
RowGroupの中にはColumnがある
Columnの中にはPageがある
Pageの中にデータの本体が入っている
Parquetはネストを全て展開して、全て別の列として扱われる (ネストや配列を入れ子 にしていない)
definition level
と repetition level
のおかげ
例えば{col1: "val1", col2: {col3: "val2"}}
というデータをParquetに書くと、ファイルの構造は下のようになります。
{
RowGroups : [
{
Column ( for "col1") : [{
Pages : [{
Header ( thrift形式) ,
Values : [
"val1" ,
...
]
}]
}]
} ,
{
Column ( for "col2. col3") : [{
Pages : [{
Header,
Values : [
"val2" ,
...
]
}]
}]
}
] ,
MetaData ( thrift)
}
Parquetの特徴
Parquetは人気な形式であり、その中身に興味を持つような人はParquetの特徴をとっくに知っていることでしょう。
ここではこの投稿に必要な特徴だけ紹介します。
列志向である
Dremel(Google の社内ツール)のファイルのフォーマットを基にしている
Parquetをhexdump
まず、構造を確認するためにファイルを作成します。
json で表すと、下のような内容です。(見当がつきやすいようにtext列にはplainエンコーディング を使用しています)
[
{ "text ": "text1 "} ,
{ "text ": "text2 "} ,
{ "text ": "text3 "} ,
{ "text ": "text4 "} ,
{ "text ": "text5 "} ,
]
ファイル作成のためのgoコード
parquet-go を使用し、以下のようなコードからファイルを作成します。
import (
"github.com/parquet-go/parquet-go"
)
type MyTypeSimple struct {
Text string `parquet:"text,plain"`
}
func write1() {
v := []MyTypeSimple{
{Text: "text1" },
{Text: "text2" },
{Text: "text3" },
{Text: "text4" },
{Text: "text5" },
}
err := parquet.WriteFile("./simple.parquet" , v)
if err != nil {
panic (err)
}
}
ファイル(307バイト)を見ると次のようになります。
$ hexdump -C simple.parquet
00000000 50 41 52 31 15 06 15 5a 15 5a 15 ab a3 84 b4 03 |PAR1...Z.Z......|
00000010 4c 15 0a 15 00 15 0a 15 00 15 00 15 00 12 00 00 |L...............|
00000020 05 00 00 00 74 65 78 74 31 05 00 00 00 74 65 78 |....text1....tex|
00000030 74 32 05 00 00 00 74 65 78 74 33 05 00 00 00 74 |t2....text3....t|
00000040 65 78 74 34 05 00 00 00 74 65 78 74 35 19 12 00 |ext4....text5...|
00000050 19 18 05 74 65 78 74 31 19 18 05 74 65 78 74 35 |...text1...text5|
00000060 15 00 19 16 00 00 19 1c 16 08 15 92 01 16 00 00 |................|
00000070 00 15 04 19 2c 48 0c 4d 79 54 79 70 65 53 69 6d |....,H.MyTypeSim|
00000080 70 6c 65 15 02 00 15 0c 25 00 18 04 74 65 78 74 |ple.....%...text|
00000090 25 00 4c 1c 00 00 00 16 0a 19 1c 19 1c 26 00 1c |%.L..........&..|
000000a0 15 0c 19 15 00 19 18 04 74 65 78 74 15 00 16 0a |........text....|
000000b0 16 92 01 16 92 01 26 08 3c 58 05 74 65 78 74 35 |......&.<X.text5|
000000c0 18 05 74 65 78 74 31 00 19 1c 15 06 15 00 15 02 |..text1.........|
000000d0 00 00 16 cc 01 15 16 16 9a 01 15 32 00 16 92 01 |...........2....|
000000e0 16 0a 19 0c 16 08 16 92 01 00 19 0c 18 37 67 69 |.............7gi|
000000f0 74 68 75 62 2e 63 6f 6d 2f 70 61 72 71 75 65 74 |thub.com/parquet|
00000100 2d 67 6f 2f 70 61 72 71 75 65 74 2d 67 6f 20 76 |-go/parquet-go v|
00000110 65 72 73 69 6f 6e 20 30 2e 32 33 2e 30 28 62 75 |ersion 0.23.0(bu|
00000120 69 6c 64 20 29 19 1c 1c 00 00 00 ba 00 00 00 50 |ild )..........P|
00000130 41 52 31 |AR1|
ASCIIを見ると、3行目からtext1
,text2
,text3
...とデータが入っていることが確認できます。
このファイルを使ってParquetの中身を紐解いていきます。
※ 見やすさのため、この記事ではバイトの位置を0x00からの値で表記します。例えば、16バイト目(03
)は、16(0x0f)
と書きます。(0列からf列までが1行、と見る)
Parquetは、公式の図にあるように、Magic Number
, RowGroup
, Footer
, Footer length
, Magic Number
で構成されています。
この章では、前章で作成したファイルのデータを基に、Parquetのデータ構造を見ていきます。
https://parquet.apache.org/docs/file-format/ より
まず最初に見つかるのが、ファイルの最初と最後にあるMagic Number
です。
前章のhexdumpの結果にも最初と最後に"PAR1(50 41 52 31)"という文字が見つかります。
さて、Parquetを読むには、まずFooterにあるファイル全体のメタデータ を参照するところから始まります。
Footerの長さが最後から数えて8~4バイト目にあるので、そこからFooterの場所を計算します。(little endian )
前章のhexdumpの例では、300~303バイト目(0x12b~0x12e)にある、186(ba 00 00 00
)がFooterの長さです。
Footerの長さがわかったので、Footerからファイルのメタデータ を抜き出します。
といっても、Footerにはメタデータ しかないので、実際にはFooterをそのまま抜き出すだけでメタデータ が取り出せます。
前章のhexdumpでは長さが186バイトなので、114(0x71)~299(0x12a)バイトがメタデータ 部分です(15 04 19 2c ~ 1c 00 00 00
部分)
Parquetのメタデータ はThriftのThriftCompactProtocol形式でシリアライズ されています。
parquet-formatリポジトリ にある定義 を基にメタデータ を読むと、以下の情報が入っているとわかります。
{
"version ": 2 ,
"schema ": [
{ "name ": "MyTypeSimple ", "num_children ": 1 } ,
{
"type ": "BYTE_ARRAY ",
"repetition_type ": "REQUIRED ",
"name ": "text ",
"converted_type ": "UTF8 ",
"logicalType ": { "STRING ": {} }
}
] ,
"num_rows ": 5 ,
"row_groups ": [
{
"columns ": [
{
"file_offset ": 0 ,
"meta_data ": {
"type ": "BYTE_ARRAY ",
"encodings ": [ "PLAIN "] ,
"path_in_schema ": [ "text "] ,
"codec ": "UNCOMPRESSED ",
"num_values ": 5 ,
"total_uncompressed_size ": 73 ,
"total_compressed_size ": 73 ,
"data_page_offset ": 4 ,
"statistics ": { "max_value ": "text5 ", "min_value ": "text1 " } ,
"encoding_stats ": [
{ "page_type ": "DATA_PAGE_V2 ", "encoding ": "PLAIN ", "count ": 1 }
]
} ,
"offset_index_offset ": 102 ,
"offset_index_length ": 11 ,
"column_index_offset ": 77 ,
"column_index_length ": 25
}
] ,
"total_byte_size ": 73 ,
"num_rows ": 5 ,
"file_offset ": 4 ,
"total_compressed_size ": 73
}
] ,
"created_by ": "github.com/parquet-go/parquet-go version 0.23.0(build ) ",
"column_orders ": [
{ "TYPE_ORDER ": {} }
]
}
メタデータ には色々な情報が入っていますが、本記事ではcolumns
の中の情報が重要です。
RowGroup, Column, Page内容の取得
前節でファイルのメタデータ を取得できました。
更にcolumnの情報を取得します。
メタデータ を見ると、row_groupsフィールドがあり、その中にcolumnsフィールドがあることがわかります。
columnsフィールドは配列です。今の例ではtext列しかないので、今回は1つです。
そのcolumnの中には最初のPageの始点(data_page_offset=4)とcolumn全体のバイト数(total_compressed_size=73)を表すフィールドがあります。
これが、text列のデータが入っている場所を示しています。
つまり、この例ではファイルの5(0x4)~77(0x4c)バイト目がcolumnの対象です。
ファイルのhexdumpを再掲します。
$ hexdump -C simple.parquet
00000000 50 41 52 31 15 06 15 5a 15 5a 15 ab a3 84 b4 03 |PAR1...Z.Z......|
00000010 4c 15 0a 15 00 15 0a 15 00 15 00 15 00 12 00 00 |L...............|
00000020 05 00 00 00 74 65 78 74 31 05 00 00 00 74 65 78 |....text1....tex|
00000030 74 32 05 00 00 00 74 65 78 74 33 05 00 00 00 74 |t2....text3....t|
00000040 65 78 74 34 05 00 00 00 74 65 78 74 35 19 12 00 |ext4....text5...|
00000050 19 18 05 74 65 78 74 31 19 18 05 74 65 78 74 35 |...text1...text5|
00000060 15 00 19 16 00 00 19 1c 16 08 15 92 01 16 00 00 |................|
00000070 00 15 04 19 2c 48 0c 4d 79 54 79 70 65 53 69 6d |....,H.MyTypeSim|
00000080 70 6c 65 15 02 00 15 0c 25 00 18 04 74 65 78 74 |ple.....%...text|
00000090 25 00 4c 1c 00 00 00 16 0a 19 1c 19 1c 26 00 1c |%.L..........&..|
000000a0 15 0c 19 15 00 19 18 04 74 65 78 74 15 00 16 0a |........text....|
000000b0 16 92 01 16 92 01 26 08 3c 58 05 74 65 78 74 35 |......&.<X.text5|
000000c0 18 05 74 65 78 74 31 00 19 1c 15 06 15 00 15 02 |..text1.........|
000000d0 00 00 16 cc 01 15 16 16 9a 01 15 32 00 16 92 01 |...........2....|
000000e0 16 0a 19 0c 16 08 16 92 01 00 19 0c 18 37 67 69 |.............7gi|
000000f0 74 68 75 62 2e 63 6f 6d 2f 70 61 72 71 75 65 74 |thub.com/parquet|
00000100 2d 67 6f 2f 70 61 72 71 75 65 74 2d 67 6f 20 76 |-go/parquet-go v|
00000110 65 72 73 69 6f 6e 20 30 2e 32 33 2e 30 28 62 75 |ersion 0.23.0(bu|
00000120 69 6c 64 20 29 19 1c 1c 00 00 00 ba 00 00 00 50 |ild )..........P|
00000130 41 52 31 |AR1|
つまり、15 06 15 5a ~ 65 78 74 35
の部分ですね。
更に、columnの最初には最初のpage用のメタデータ (Page Header)が入っています。
このメタデータ もparquet-formatで定義されているthriftなので、Footerと同じようにデシリアライズ すると、次のデータが取得できます。
{
"type ": "DATA_PAGE_V2 ",
"uncompressed_page_size ": 45 ,
"compressed_page_size ": 45 ,
"crc ": -457214166 ,
"data_page_header_v2 ": {
"num_values ": 5 ,
"num_nulls ": 0 ,
"num_rows ": 5 ,
"encoding ": "PLAIN ",
"definition_levels_byte_length ": 0 ,
"repetition_levels_byte_length ": 0 ,
"is_compressed ": false
}
}
このthriftが終わるのが32(0x1f)バイト目なので、compressed_page_sizeの値(45)を考えた、33(0x20)から77(0x4c)バイト目がデータの本体だとわかります。(05 00 00 00 ~ 65 78 74 35
)
データの取得
前節で33(0x20)から77(0x4c)バイトがデータの部分とわかりました。
データのバイナリは05 00 00 00 74 65 78 74 31 05 00 00 00 74 65 78 74 32...
となっています。
このファイルはplainエンコーディング を使用しているので、最初に4バイトで文字長があり、その後に文字が続きます。
つまり、最初が5バイトのテキスト(74 65 78 74 31
=text1)、次も5バイトのテキスト・・・と続きます。
なので、データには[text1, text2, text3, text4, text5]
が入っているとわかるわけです。
ファイルには以下のデータを入れているので、データが正しく取得できたことがわかります。
[
{ "text ": "text1 "} ,
{ "text ": "text2 "} ,
{ "text ": "text3 "} ,
{ "text ": "text4 "} ,
{ "text ": "text5 "} ,
]
データにアクセスできた
これで、Parquetからデータを取り出すことができました。
Parquetには「列の値が連続で格納されている」ことと「メタデータ を辿ることでデータにアクセスできる」ことがわかりました。
これで大まかな構造がわかりました。
しかし、これだけではありません。
Parquetはネストと配列がサポートされています。
ネストや配列のデータを取り出すには、更にもう1段階深堀りする必要があります。
更に先に進むため、章を分けて、もう少し続きます。
複雑なデータ構造に対応する
前章では簡単なデータ構造を持つParquetの構造を見ました。
しかし、Parquetはネストと配列を扱うことができます。
これらは、definition level
とrepetition level
という仕組みによって、通常のフィールドと同じように1列で表されています。
つまり、下のようにネストや配列があっても
{
col1 : "val1" ,
col2 : { col3 : "val3" } ,
col4 : { col5 : { col6 : [ 1 , 2 , 3 ]}}
}
下のように同じレベルで保存されるのです。
{
"col1" : [ "val1" ] ,
"col2.col3" : [ "val3" ] ,
"col4.col5.col6" : [ 1 , 2 , 3 ]
}
この章ではdefinition level
とrepetition level
という仕組みを理解して、複雑なデータ構造を持つParquetを読み解きます。
ちなみに、この2つのレベルに関する説明はParquetのドキュメントには見つからなかったのですが、Dremelの論文をあたるとわかります
https://research.google/pubs/dremel-interactive-analysis-of-web-scale-datasets-2/
definition levelとrepetition level
まず、definition level
とrepetition level
の仕組みについて説明します。
雑に言うと、
definition level
はnullable(optional)なのにnullならなかった親フィールドの数で、
repetition level
は繰り返しがあったフィールドのレベルを表しています。
意味がわからないと思うので例をつけて解説します。
definition level
まず、definition level
について。
次のjson で表されるデータが入っているとします、全てのフィールドがoptional(nullable)です。
{
nest1 : {
nest2 : {
value : "hello"
}
} ,
} ,
{
nest1 : {
nest2 : {
value : null
}
} ,
} ,
{
nest1 : {
nest2 : null
}
} ,
{
nest1 : null
} ,
{}
このとき、各フィールドのdefinition level
はフィールド横のコメントの値(3,2,1,0,0
)です。
definition level
を求めると、
最初の値(hello)は「nest1, nest2, value の3フィールドがoptionalなのにnullではなかったから、definition level
は3」となります。
同様に、2つめは「nest1, nest2がoptionalなのにnullではなかったから2(value はnullだからカウントしない)」となります。
3つ目は「nest1だけnullじゃないから1」です。
最後の2つはnest1すらnullなので0です。
このように、optionalな親フィールドの数を表すのがdefinition level
です。
この仕組みによって、対象columnだけをスキャンしてもdefinition level
をみればどこの親までnullになっているかがわかります。
repetition level
次に、repetition level
について。
次のjson で表されるデータが入っているとします。
{
repeated1 : [
{
repeated2 : [
"value1-1" ,
"value1-2" ,
"value1-3"
] ,
normalField2 : "v1"
} ,
{
repeated2 : [
"value2-1" ,
"value2-2"
]
normalField2 : "v2"
} ,
{
repeated2 : [
"value3-1" ,
] ,
normalField2 : "v3"
}
] ,
normalField1 : "v4"
} ,
このとき、repetition level
はコメントにあるRの値です。
repetition level
を求めると、
最初のvalue1-1は「それ以前に配列(repeated)のフィールドがなかったから0」です。
次のvalue1-2は「repeated2の親のrepeated1も配列であるから、repeated2のレベルは2。value1-1があるから、repeated2には繰り返しがあった。よって、value1-2のrepetition level
はrepeated2のレベルから、2」となります。
次のvalue1-3も「value1-2があるから、repeated2に繰り返しがあった。よって2」です。
対して、value2-1は「repeated2には繰り返しがないが、repeated1には繰り返しがある(value1-1などの分の要素がある)。repeated1のレベルから、1」。
更にvalue2-2は「value2-1があるから、repeated2に繰り返しがあった。よって2」。
value3-1はvalue2-1と同様に、「repeated2には繰り返しがないが、repeated1には繰り返しがある。repeated1のレベルから、1」。
と決定します。
また、自身が配列でない場合でも親に配列があれば影響されるので、
v1は「それ以前に配列(repeated)のフィールドがなかったから0」
v2とv3は「repeated1が配列として機能しているから、repeated1のレベルから、1」
v4は「親に配列がないから、0」。
と、repetition level
の値が付きます。
このように、配列として機能したフィールドのレベルを表すのがrepetition level
です。
この仕組みによって、対象columnだけをスキャンしてもrepetition level
を参照することで配列の最初かどうか、親が変わったか、などがわかります。
hexdump
これまでdefinition level
とrepetition level
について見てきたので、ネストや配列があるファイルを読むことができるようになりました。
実際のデータをバイナリで見てみましょう。
以下のデータを使用します。
[
{
"nest ": {
"nest ": "nest1 ",
"repeated ": [
{ "nest ": { "repeated ": [ "nestRep1 ", "nestRep2 ", "nestRep3 "]}} ,
{ "nest ": { "repeated ": [ "nestRep4 ", "nestRep5 "]}} ,
]
},
} ,
{
"nest ": {
"nest ": "nest2 ",
"repeated ": [
{ "nest ": { "repeated ": [ "nestRep6 "]}} ,
]
},
} ,
{
"nest ": {
"nest ": null ,
"repeated ": []
} ,
},
]
このデータはparquet内では次のようなイメージで配置され、それぞれの値にdefinition level
とrepetition level
の値が振られます。
[
{ "nest.nest" : [
"nest1" ,
"nest2" ,
null ,
]} ,
{ "nest.repeated.nest.repeated" : [
"nestRep1" ,
"nestRep2" ,
"nestRep3" ,
"nestRep4" ,
"nestRep5" ,
"nestRep6" ,
null
]}
]
この情報をparquetから確認します。
データをparquetにしてhexdumpで見ると、下のようになっています。
00000000 50 41 52 31 15 06 15 2c 15 2c 15 df cf fe db 0d |PAR1...,.,......|
00000010 4c 15 06 15 02 15 06 15 00 15 08 15 00 12 00 00 |L...............|
00000020 04 02 02 01 05 00 00 00 6e 65 73 74 31 05 00 00 |........nest1...|
00000030 00 6e 65 73 74 32 15 06 15 ac 01 15 ac 01 15 bc |.nest2..........|
00000040 81 ac e6 09 4c 15 0e 15 02 15 06 15 00 15 08 15 |....L...........|
00000050 14 12 00 00 02 00 04 02 02 01 02 02 04 00 0c 04 |................|
00000060 02 01 08 00 00 00 6e 65 73 74 52 65 70 31 08 00 |......nestRep1..|
00000070 00 00 6e 65 73 74 52 65 70 32 08 00 00 00 6e 65 |..nestRep2....ne|
00000080 73 74 52 65 70 33 08 00 00 00 6e 65 73 74 52 65 |stRep3....nestRe|
00000090 70 34 08 00 00 00 6e 65 73 74 52 65 70 35 08 00 |p4....nestRep5..|
000000a0 00 00 6e 65 73 74 52 65 70 36 19 12 00 19 18 05 |..nestRep6......|
000000b0 6e 65 73 74 31 19 18 05 6e 65 73 74 32 15 00 19 |nest1...nest2...|
000000c0 16 02 00 19 12 00 19 18 08 6e 65 73 74 52 65 70 |.........nestRep|
000000d0 31 19 18 08 6e 65 73 74 52 65 70 36 15 00 19 16 |1...nestRep6....|
000000e0 02 00 19 1c 16 08 15 64 16 00 00 00 19 1c 16 6c |.......d.......l|
000000f0 15 e8 01 16 00 00 00 15 04 19 6c 48 06 4d 79 44 |..........lH.MyD|
00000100 65 65 70 15 02 00 35 02 18 04 6e 65 73 74 15 04 |eep...5...nest..|
00000110 00 15 0c 25 02 18 04 6e 65 73 74 25 00 4c 1c 00 |...%...nest%.L..|
00000120 00 00 35 04 18 08 72 65 70 65 61 74 65 64 15 02 |..5...repeated..|
00000130 00 35 02 18 04 6e 65 73 74 15 02 00 15 0c 25 04 |.5...nest.....%.|
00000140 18 08 72 65 70 65 61 74 65 64 25 00 4c 1c 00 00 |..repeated%.L...|
00000150 00 16 06 19 1c 19 2c 26 00 1c 15 0c 19 25 00 06 |......,&.....%..|
00000160 19 28 04 6e 65 73 74 04 6e 65 73 74 15 00 16 06 |.(.nest.nest....|
00000170 16 64 16 64 26 08 3c 36 02 28 05 6e 65 73 74 32 |.d.d&.<6.(.nest2|
00000180 18 05 6e 65 73 74 31 00 19 1c 15 06 15 00 15 02 |..nest1.........|
00000190 00 00 16 c4 03 15 14 16 d4 02 15 32 00 26 00 1c |...........2.&..|
000001a0 15 0c 19 25 00 06 19 48 04 6e 65 73 74 08 72 65 |...%...H.nest.re|
000001b0 70 65 61 74 65 64 04 6e 65 73 74 08 72 65 70 65 |peated.nest.repe|
000001c0 61 74 65 64 15 00 16 0e 16 e8 01 16 e8 01 26 6c |ated..........&l|
000001d0 3c 36 02 28 08 6e 65 73 74 52 65 70 36 18 08 6e |<6.(.nestRep6..n|
000001e0 65 73 74 52 65 70 31 00 19 1c 15 06 15 00 15 02 |estRep1.........|
000001f0 00 00 16 d8 03 15 16 16 86 03 15 3e 00 16 cc 02 |...........>....|
00000200 16 06 19 0c 16 08 16 cc 02 00 19 0c 18 37 67 69 |.............7gi|
00000210 74 68 75 62 2e 63 6f 6d 2f 70 61 72 71 75 65 74 |thub.com/parquet|
00000220 2d 67 6f 2f 70 61 72 71 75 65 74 2d 67 6f 20 76 |-go/parquet-go v|
00000230 65 72 73 69 6f 6e 20 30 2e 32 33 2e 30 28 62 75 |ersion 0.23.0(bu|
00000240 69 6c 64 20 29 19 2c 1c 00 00 1c 00 00 00 57 01 |ild ).,.......W.|
00000250 00 00 50 41 52 31 |..PAR1|
00000256
最初に、フッターのメタデータ は343(0x0157)バイトあるので、248(0xf7=598-343-8+1)バイト目から(15 04 19 6c
から)始まることがわかります。
このメタデータ をthriftにしてcolumnの情報に注目すると、
{
"version ": 2 ,
"row_groups ": [
{
"columns ": [
{
"meta_data ": {
"path_in_schema ": [ "nest ", "nest "] ,
"total_compressed_size ": 50 ,
"data_page_offset ": 4 ,
},
} ,
{
"meta_data ": {
"path_in_schema ": [ "nest ", "repeated ", "nest ", "repeated "] ,
"total_compressed_size ": 121 ,
"data_page_offset ": 54 ,
},
}
],
}
],
}
と書いてあります。
そこから更に"nest.nest"のcolumnのメタデータ (5バイト目から始まる)にあるカラムの情報を見ると、
{
"data_page_header_v2 ": {
"num_values ": 3 ,
"num_nulls ": 1 ,
"num_rows ": 3 ,
"encoding ": "PLAIN ",
"definition_levels_byte_length ": 4 ,
"repetition_levels_byte_length ": 0 ,
"is_compressed ": false
}
}
と書かれていて、definition level
の4バイトで表され、repetition level
は書かれていない(0バイト)ことがわかります。
repetition level
が書かれていないのは、全てがゼロなので、省略されているからです。
definition level
は4バイトで、33バイト目から36バイト目(0x20~0x23)の04 02 02 01
が該当箇所です。
04 02 02 01
は2進数にすると00000100 00000010 00000010 00000001
です。
definition level
とrepetition level
はRunLengthかBitPackingエンコーディング されています。
https://parquet.apache.org/docs/file-format/data-pages/encodings/ を見ると、データはヘッダーと値の2バイトで構成されていて、ヘッダー(前半1バイト)の8ビット目が0のときRunLengthエンコーディング されていて、1ならBitPackエンコーディング されているとわかります。
今回の例では1バイト目と3バイト目のヘッダーは両方0で終わるのでRunLengthで、ヘッダーの最後尾1ビットを除くことで0000010(0) 00000010 0000001(0) 00000001
は「2(0b10)回2(0b10)の後、1(0b1)回1(0b1)」と解釈してdefinition level
は2,2,1
とわかります。
次に"nest.repeated.nest.repeated"のcolumnのメタデータ の情報を見ると
{
"data_page_header_v2 ": {
"num_values ": 7 ,
"num_nulls ": 1 ,
"num_rows ": 3 ,
"encoding ": "PLAIN ",
"definition_levels_byte_length ": 4 ,
"repetition_levels_byte_length ": 10 ,
"is_compressed ": false
}
}
と書かれていて、definition level
が4バイトで、repetition level
が10バイトとわかります。
repetition level
が先に書かれるのでrepetition level
から見ると、
データは85(0x54)バイト目から始まるので、repetition level
は85(0x54)~94(0x5d)バイト目で、0000001(0) 00000000 0000010(0) 00000010 0000001(0) 00000001 0000001(0) 00000010 0000010(0) 00000000
(02 00 04 02 02 01 02 02 04 00
)です。
展開すると0,2,2,1,2,0,0
です。
definition level
はその後の4バイトなので、95(0x5e)~98(0x61)0000110(0) 00000100 0000001(0) 00000001
(0C 04 02 01
)です。展開すると4,4,4,4,4,4,1
とわかります。
まとめると、
"nest.nest"の列はdefinition level
が2,2,1
、repetition level
が0,0,0
"nest.repeated.nest.repeated"の列はdefinition level
が4,4,4,4,4,4,1
、repetition level
が0,2,2,1,2,0,0
と読むことができました。
これらの値は、はじめに出したDとRに一致しています。
[
{ "nest.nest" : [
"nest1" ,
"nest2" ,
null ,
]} ,
{ "nest.repeated.nest.repeated" : [
"nestRep1" ,
"nestRep2" ,
"nestRep3" ,
"nestRep4" ,
"nestRep5" ,
"nestRep6" ,
null
]}
]
これで、ネストや配列があってもparquetを読み解けるようになりました。
最後に
これで、Paruetファイルはメタデータ が含まれていて、各フィールドは全て列で表されていることがわかりました。
より詳細な話は、以下のページを参考にしてください。
https://parquet.apache.org/docs/file-format/
https://github.com/apache/parquet-format
また、ParquetはDremelの中で使われているファイルのフォーマットを参考にしていることから、公式にも「Dremelのxxxを使って・・・」という話が出てきます(特にdefinition level
とrepetition level
)。その場合は、Dremelの論文を読むとわかります。
Dremel: Interactive Analysis of Web-Scale Datasets
https://research.google/pubs/dremel-a-decade-of-interactive-sql-analysis-at-web-scale/
以上、Parquetの構造の紹介でした。