列存标准格式

列存文件格式Parquet与Orc

列存与行存的区别

1
2
3
4
5
6
7
8
对于一张表数据
行存
张三|23|男,李四|25|男,王五|27|男
列存
张三|李四|王五,23|25|27,男|男|男

行存适用于数据整行读取场景
列存适用于部分列读取场景

Parquet

Schema协议

1
2
3
4
schema最上层是message,包含一系列字段
每个字段拥有3个属性:repetition(重复性),type(类型),name(名称)
字段的类型可以是group(嵌套),原子类型(int,bool,string等)
重复性有三种情况:required(有且只有一次),optional(0或1次),repeated(0或多次)

存储格式

1
2
3
4
5
6
7
8
9
对于一条记录(Record),先按列(Column)进行拆分
一个字段对应一列
列存连续的存储一个字段的值,以便高效编码压缩达到快速读取

Block(HDFS Block): Parquet的设计与HDFS完全兼容
File: HDFS文件,保存了文件的元数据信息,可以不包含实际数据(Block保存实际数据)
Row Group: 按照行将数据划分为多个逻辑水平分区,一个行组由每一个列块(Column Chunk)组成
Column Chunk: 列块,分布在行组中,在文件中保证是连续的
Page: 一个列块分成多个Pages(页面),页面是Parquet中最小的基础单元

文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Parquet是自解析的,Thrift格式定义的文件schema和其他元数据一起存储在文件末尾
文件格式参考:
4-byte magic number "PAR1"
<Column 1 Chunk 1 + Column Metadata>
<Column 2 Chunk 1 + Column Metadata>
...
<Column N Chunk 1 + Column Metadata>
<Column 1 Chunk 2 + Column Metadata>
<Column 2 Chunk 2 + Column Metadata>
...
<Column N Chunk 2 + Column Metadata>
...
<Column 1 Chunk M + Column Metadata>
<Column 2 Chunk M + Column Metadata>
...
<Column N Chunk M + Column Metadata>
File Metadata
4-byte length in bytes of file metadata
4-byte magic number "PAR1"

整个文件(表)N个列,划分了M个行组,每个行组都有所有列的一个Chunk和其元数据信息
读取时首先从文件末尾读取文件元数据信息,再在其中找到感兴趣的列块信息

---

这里给出一份实际的数据,Hive Parquet表,列为name,age
create table test (name string,age bigint) stored as parquet
数据为Jack,19
直接cat
PAR1JackJack(JackJackJ<Hk,
hive_schema
%name%%age,&
namevv<JackJack(JackJack&~age��wriAsia/ShanghaiJparquet-mr version 1.10.0 (build 031a6654009e3b82020012a18434c582bd74c73a),EPAR1

利用parquet-tools工具
parquet-tools meta 000000_0 >> meta.txt
file: file:/Users/xz/Downloads/000000_0
creator: parquet-mr version 1.10.0 (build 031a6654009e3b82020012a18434c582bd74c73a)
extra: writer.time.zone = Asia/Shanghai

file schema: hive_schema
--------------------------------------------------------------------------------
name: OPTIONAL BINARY L:STRING R:0 D:1
age: OPTIONAL INT64 R:0 D:1

row group 1: RC:1 TS:134 OFFSET:4
--------------------------------------------------------------------------------
name: BINARY UNCOMPRESSED DO:0 FPO:4 SZ:59/59/1.00 VC:1 ENC:RLE,BIT_PACKED,PLAIN ST:[min: Jack, max: Jack, num_nulls: 0]
age: INT64 UNCOMPRESSED DO:0 FPO:63 SZ:75/75/1.00 VC:1 ENC:RLE,BIT_PACKED,PLAIN ST:[min: 19, max: 19, num_nulls: 0]

parquet-tools cat 000000_0
name = Jack
age = 19

重复级别与定义级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
需要无损保留嵌套格式的结构化信息
只有字段值无法表达清除记录的结构

Repetition Level: 用来表示该字段路径上哪个节点进行了重复
Definition Level: 用来表示该字段路径上有多少可选的字段实际进行了定义

---

假如有两行数据
schema:
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;
}
}
这里改成表结构来看
docid int
links<backward array,forward array> array
name<language<(code string,country string) map>,url string>

record1
DocId: 10
Links
-- 补充位 BackWard: null
Forward: 20
Forward: 40
Forward: 60
Name
Language
Code: 'en-us'
Country: 'us'
Language
Code: 'en'
-- 补充位 Country: null
Url: 'http://A'
Name
-- 补充位 Language
-- 补充位 Code: null
-- 补充位 Country: null
Url: 'http://B'
Name
Language
Code: 'en-gb'
Country: 'gb'
-- 补充位 Url: null

record2
DocId: 20
Links
Backward: 10
Backward: 30
Forward: 80
Name
-- 补充位 Language
-- 补充位 Code: null
-- 补充位 Country: null
Url: 'http://C'

---

从上到下依次对应的rl和dl
DocId(属性为required,不参与dl计算,所以dl=1-1=0)
value:10,rl:0,dl:0
value:20,rl:0,dl:0

Links.Backward
value:null,rl:0,dl:1
value:10,rl:0,dl:2
value:30,rl:1,dl:2
对于Links.Backward,总共3个值
record1中为null,rl没有重复所以为0,dl因为没有值所以取上一个层次的深度为1
record2中为10,新行,rl为0,dl为实际深度为2
record2中为30,在节点Links重复,rl为1,dl为实际深度为2

Links.Forward
value:20,rl:0,dl:2
value:40,rl:1,dl:2
value:60,rl:1,dl:2
value:80,rl:0,dl:2
record1中为20,新行,rl为0,dl为实际深度为2
record1中为40,在节点Links重复,rl为1,dl为实际深度为2
record1中为60,在节点Links重复,rl为1,dl为实际深度为2
record2中为80,新行,rl为0,dl为实际深度为2

Name.Language.Code
value:en-us,rl:0,dl:2
value:en,rl:2,dl:2
value:null,rl:1,dl:1
value:en-gb,rl:1,dl:2
value:null,rl:0,dl:1
record1中为en-us,新行,rl为0,dl为实际深度为2(这里解释一下为啥是2,不是3么,因为code是required属性,一定会被定义,所以不参与计算)
record1中为en,重复所属节点Language位置,rl为2,dl为实际深度为2
record1中为null,重复所属节点Name位置,rl为1,dl因为没有值所以取上一个层次的深度为1
record1中为en-gb,重复所属节点Name位置,rl为1,dl为实际深度为2
record2中为null,新行,rl为0,dl取Name位置为1

Name.Language.Country
value:us,rl:0,dl:3
value:null,rl:2,dl:2
value:null,rl:1,dl:1
value:gb,rl:1,dl:3
value:null,rl:0,dl:1
record1中为us,新行,rl为0,dl为3
record1中为null,在节点Language重复,rl为2,dl为2
record1中为null,在节点Name重复,rl为1,dl为1
record1中为gb,在节点Name重复,rl为1,dl为3
record2中为null,新行,rl为0,在Name处重复,dl为1

Name.Url
value:http://A,rl:0,dl:2
value:http://B,rl:1:dl:2
value:null,rl:1:dl:1
value:http://C,rl:0:dl:2
record1中为http://A,新行,rl为0,dl为2
record1中为http://B,在节点Name重复,rl为1,dl为2
record1中为null,在节点Name重复,rl为1,dl为1
record2中为http://C,新行,rl为0,dl为2

总结:
在计算rl时,其实就看这个重复是发生在什么节点上的
record1的Code
Name.Language.Code: 'en-us'
Name.Language.Code: 'en'
这两个Code因为属于同一个Name下不同的Language,所以en的rl为2
在计算dl时,其实就是在算整条路径下除了required属性之外的节点数
这也是为啥Name.Language.Code论层次有3层,但是dl缺是2的原因

文件损坏情况

1
2
3
4
文件元数据损坏: 整个文件数据丢失
列元数据损坏: 该列块数据丢失,不影响其他列块使用
Page Header损坏: 列块剩余页面丢失
页面数据损坏: 该页面数据丢失

Parquet配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
https://github.com/apache/parquet-format#configurations
行组大小(row group size): 大行组允许大列块,同时意味着更大的写缓存,推荐行组大小512MB-1GB,通常建议一个HDFS Block对应一个行组,所以需要修改HDFS Block大小

数据页大小(data page size): 数据页小,带来细粒度读取,数据页大,减少空间开销与解析开销,推荐页面大小8KB

合理调参能极大的优化执行效率
https://github.com/apache/parquet-mr/blob/master/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetOutputFormat.java
public static final String JOB_SUMMARY_LEVEL = "parquet.summary.metadata.level";
public static final String BLOCK_SIZE = "parquet.block.size";
public static final String PAGE_SIZE = "parquet.page.size";
public static final String COMPRESSION = "parquet.compression";
public static final String WRITE_SUPPORT_CLASS = "parquet.write.support.class";
public static final String DICTIONARY_PAGE_SIZE = "parquet.dictionary.page.size";
public static final String ENABLE_DICTIONARY = "parquet.enable.dictionary";
public static final String VALIDATION = "parquet.validation";
public static final String WRITER_VERSION = "parquet.writer.version";
public static final String MEMORY_POOL_RATIO = "parquet.memory.pool.ratio";
public static final String MIN_MEMORY_ALLOCATION = "parquet.memory.min.chunk.size";
public static final String MAX_PADDING_BYTES = "parquet.writer.max-padding";
public static final String MIN_ROW_COUNT_FOR_PAGE_SIZE_CHECK = "parquet.page.size.row.check.min";
public static final String MAX_ROW_COUNT_FOR_PAGE_SIZE_CHECK = "parquet.page.size.row.check.max";
public static final String ESTIMATE_PAGE_SIZE_CHECK = "parquet.page.size.check.estimate";
public static final String COLUMN_INDEX_TRUNCATE_LENGTH = "parquet.columnindex.truncate.length";
public static final String STATISTICS_TRUNCATE_LENGTH = "parquet.statistics.truncate.length";
public static final String BLOOM_FILTER_ENABLED = "parquet.bloom.filter.enabled";
public static final String BLOOM_FILTER_EXPECTED_NDV = "parquet.bloom.filter.expected.ndv";
public static final String BLOOM_FILTER_MAX_BYTES = "parquet.bloom.filter.max.bytes";
public static final String PAGE_ROW_COUNT_LIMIT = "parquet.page.row.count.limit";
public static final String PAGE_WRITE_CHECKSUM_ENABLED = "parquet.page.write-checksum.enabled";

Orc

文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
https://orc.apache.org/specification/ORCv2/
Orc文件和Parquet是一样的自解析
与Parquet不同,Orc原生不支持嵌套数据格式,但是其包含复杂数据类型
并且Orc支持布隆过滤

Orc文件: 二进制文件,一个文件包含多个stripe,每一个stripe包含多条记录
stripe: 一行组形成一个stripe,每次读取文件都是以行组为单位
stripe元数据: 保存stripe位置,每一列在该stripe的统计信息,类型及位置
row group: 索引最小单位,一个stripe中包含多个row grou,默认为10000个值组成
stream: 一个stream表示文件中一段有效的数据,索引stream保存每一个row group的位置和统计信息,数据stream保存多种类型的数据

---

文件格式
[Index Data
Row Data
Stripe Footer]
......
[Index Data
Row Data
Stripe Footer]
File Footer
Postscript

---

使用orc-tools工具
orc-tools meta orc
Processing data file orc [length: 301]
Structure for orc
File Version: 0.12 with ORC_517 by ORC Java
Rows: 1
Compression: ZLIB
Compression size: 262144
Calendar: Julian/Gregorian
Type: struct<name:string,age:bigint>

Stripe Statistics:
Stripe 1:
Column 0: count: 1 hasNull: false
Column 1: count: 1 hasNull: false bytesOnDisk: 13 min: Mark max: Mark sum: 4
Column 2: count: 1 hasNull: false bytesOnDisk: 6 min: 21 max: 21 sum: 21

File Statistics:
Column 0: count: 1 hasNull: false
Column 1: count: 1 hasNull: false bytesOnDisk: 13 min: Mark max: Mark sum: 4
Column 2: count: 1 hasNull: false bytesOnDisk: 6 min: 21 max: 21 sum: 21

Stripes:
Stripe: offset: 3 data: 19 rows: 1 tail: 46 index: 65
Stream: column 0 section ROW_INDEX start: 3 length 11
Stream: column 1 section ROW_INDEX start: 14 length 30
Stream: column 2 section ROW_INDEX start: 44 length 24
Stream: column 1 section DATA start: 68 length 7
Stream: column 1 section LENGTH start: 75 length 6
Stream: column 2 section DATA start: 81 length 6
Encoding column 0: DIRECT
Encoding column 1: DIRECT_V2
Encoding column 2: DIRECT_V2

File length: 301 bytes
Padding length: 0 bytes
Padding ratio: 0%

orc-tools data orc
Processing data file orc [length: 301]
{"name":"Mark","age":21}