我們知道parquet文件格式是不能進(jìn)行update操作的。但是是否可以對(duì)其進(jìn)行添加一列數(shù)據(jù)呢?
先看看parquet文件長(zhǎng)什么樣
Parquet文件是以二進(jìn)制方式存儲(chǔ)的忧设,是不可以直接讀取和修改的古沥,Parquet文件是自解析的彩扔,文件中包括該文件的數(shù)據(jù)和元數(shù)據(jù)。在HDFS文件系統(tǒng)和Parquet文件中存在如下幾個(gè)概念:
- HDFS塊(Block):它是HDFS上的最小的副本單位蒸痹,HDFS會(huì)把一個(gè)Block存儲(chǔ)在本地的一個(gè)文件并且維護(hù)分散在不同的機(jī)器上的多個(gè)副本春弥,通常情況下一個(gè)Block的大小為256M、512M等叠荠。
- HDFS文件(File):一個(gè)HDFS的文件匿沛,包括數(shù)據(jù)和元數(shù)據(jù),數(shù)據(jù)分散存儲(chǔ)在多個(gè)Block中榛鼎。
- 行組(Row Group):按照行將數(shù)據(jù)物理上劃分為多個(gè)單元逃呼,每一個(gè)行組包含一定的行數(shù)鳖孤,在一個(gè)HDFS文件中至少存儲(chǔ)一個(gè)行組,Parquet讀寫的時(shí)候會(huì)將整個(gè)行組緩存在內(nèi)存中抡笼。
- 列塊(Column Chunk):在一個(gè)行組中每一列保存在一個(gè)列塊中苏揣,行組中的所有列連續(xù)的存儲(chǔ)在這個(gè)行組文件中。不同的列塊可能使用不同的算法進(jìn)行壓縮推姻。
- 頁(yè)(Page):每一個(gè)列塊劃分為多個(gè)頁(yè)平匈,一個(gè)頁(yè)是最小的編碼的單位,在同一個(gè)列塊的不同頁(yè)可能使用不同的編碼方式藏古。
通常情況下增炭,在存儲(chǔ)Parquet數(shù)據(jù)的時(shí)候會(huì)按照HDFS的Block大小設(shè)置行組的大小,由于一般情況下每一個(gè)Mapper任務(wù)處理數(shù)據(jù)的最小單位是一個(gè)Block拧晕,這樣可以把每一個(gè)行組由一個(gè)Mapper任務(wù)處理隙姿,增大任務(wù)執(zhí)行并行度。
上圖展示了一個(gè)Parquet文件的結(jié)構(gòu)防症,一個(gè)文件中可以存儲(chǔ)多個(gè)行組孟辑,文件的首位都是該文件的Magic Code,用于校驗(yàn)它是否是一個(gè)Parquet文件蔫敲,F(xiàn)ooter length存儲(chǔ)了文件元數(shù)據(jù)的大小饲嗽,通過(guò)該值和文件長(zhǎng)度可以計(jì)算出元數(shù)據(jù)的偏移量,文件的元數(shù)據(jù)中包括每一個(gè)行組的元數(shù)據(jù)信息和當(dāng)前文件的Schema信息奈嘿。除了文件中每一個(gè)行組的元數(shù)據(jù)貌虾,每一頁(yè)的開始都會(huì)存儲(chǔ)該頁(yè)的元數(shù)據(jù),在Parquet中裙犹,有三種類型的頁(yè):數(shù)據(jù)頁(yè)尽狠、字典頁(yè)和索引頁(yè)。數(shù)據(jù)頁(yè)用于存儲(chǔ)當(dāng)前行組中該列的值叶圃,字典頁(yè)存儲(chǔ)該列值的編碼字典袄膏,每一個(gè)列塊中最多包含一個(gè)字典頁(yè),索引頁(yè)用來(lái)存儲(chǔ)當(dāng)前行組下該列的索引掺冠,目前Parquet中還不支持索引頁(yè)沉馆,但是在后面的版本中增加。
從parquet文件格式中可以看出德崭,如果我們需要為文件添加一列數(shù)據(jù)的話斥黑,需要對(duì)每個(gè)row group 添加列,這樣勢(shì)必會(huì)打破原有的數(shù)據(jù)偏移量眉厨,也有可能因?yàn)槎嗔艘涣性斐珊罄m(xù)的row group全部變化锌奴,字典頁(yè)和索引頁(yè)也可能造成變化;另外在Footer中也需要根據(jù)新的數(shù)據(jù)列重新對(duì)每個(gè)偏移量進(jìn)行計(jì)算憾股。
所以基于此鹿蜀,并未看到parquet在加列操作中箕慧,是在原文件中添加(甚至在使用impala對(duì)table加列后,原parquet文件schema都未曾變化)茴恰,而往往是新生成一個(gè)parquet文件销钝。
示例
原表 entry, uuid, age, name 三個(gè)字段,建表指定parquet存儲(chǔ)琐簇,并插入兩條數(shù)據(jù),在hdfs上生成一個(gè)parquet文件座享。
然后在impala執(zhí)行add column操作婉商,添加tel字段,并向其插入一些數(shù)據(jù)渣叛,生成一個(gè)新的parquet文件丈秩。通過(guò)parquet-Hadoop API直接讀取parquet文件信息,獲得
uuid->age->name->
row count: 2
{"uuid":"1",age":"20",name":"bob"}
{"uuid":"2",age":"10",name":"tom"}
uuid->age->name->tel->
row count: 6
{"uuid":"1",age":"20",name":"bob",tel":"186152372"}
{"uuid":"2",age":"30",name":"tom",tel":"186152372"}
{"uuid":"3",age":"40",name":"laiwb2",tel":"186152372"}
{"uuid":"4",age":"22",name":"bingo1",tel":"186152372"}
{"uuid":"6",age":"23",name":"feng",tel":"186152372"}
{"uuid":"7",age":"24",name":"bixians",tel":"186152372"}
從打印的信息來(lái)看淳衙,parquet文件1 schema只有三個(gè)字段uuid,age,name蘑秽,這個(gè)文件就是建表時(shí)插入的,后面在add column后又插入了6條數(shù)據(jù)箫攀,這時(shí)肠牲,新生成的parquet文件的metadata多了一個(gè)tel,數(shù)據(jù)頁(yè)中數(shù)據(jù)也多了tel列。
讀取parquet文件主要的代碼片段:
ParquetMetadata readFooter = ParquetFileReader.readFooter(conf, path, ParquetMetadataConverter.NO_FILTER);
// parquet 文件的 schema信息
MessageType schema = readFooter.getFileMetaData().getSchema();
List<Type> columnInfos = schema.getFields();
for (Type type : columnInfos) {
System.out.print(type.getName() + "->");
}
// 每個(gè) row group 的數(shù)量
List<BlockMetaData> blockMeta = readFooter.getBlocks();
for (BlockMetaData bl : blockMeta) {
System.out.println("row count: " + bl.getRowCount());
}
ParquetReader<Group> reader = ParquetReader.builder(new GroupReadSupport(), path).withConf(conf).build();
int count = 0;
Group recordData = reader.read();
while (count < 10 && recordData != null) {
StringBuilder builder = new StringBuilder();
builder.append("{\"");
for (int j = 0; j < columnInfos.size(); j++) {
if (j < columnInfos.size() - 1) {
String columnName = columnInfos.get(j).getName();
String value = recordData.getValueToString(j, 0);
builder.append(columnName + "\":\"" + value + "\",");
} else {
String columnName = columnInfos.get(j).getName();
String value = recordData.getValueToString(j, 0);
builder.append(columnName + "\":\"" + value + "\"}");
}
}
System.out.println(builder.toString());
count++;
recordData = reader.read();
}
然后drop name字段(parquet文件不會(huì)有什么變動(dòng)靴跛,只是元數(shù)據(jù)信息變化)缀雳,在hive元數(shù)據(jù)中只有uuid,age,tel三個(gè)字段,使用impala查詢和hive查詢出現(xiàn)了不同數(shù)據(jù)梢睛。hive能夠查詢匹配出這三個(gè)字段的數(shù)據(jù)來(lái)肥印,但impala卻查詢出的是uuid, age, name數(shù)據(jù),看樣子impala是根據(jù)元數(shù)據(jù)信息逐個(gè)取值的绝葡,并不是根據(jù)hive元數(shù)據(jù)和parquet元數(shù)據(jù)對(duì)應(yīng)取值深碱。
綜上,parquet 理論上可以追加一列藏畅,但是代價(jià)比較高敷硅,需要對(duì)row group 進(jìn)行修改,并且需要同步parquet metadata信息墓赴,修改數(shù)據(jù)的offerset和schema等竞膳,并且parquet-Hadoop也未提供相應(yīng)的方法。
后續(xù)~~~
列式存儲(chǔ)的另外一種存儲(chǔ)格式ORC诫硕,與parquet的對(duì)比圖坦辟。
從parquet與orc對(duì)比圖中可以知道,orc支持ACID和update操作章办,接下來(lái)說(shuō)說(shuō)ORC锉走。
和Parquet類似滨彻,它并不是一個(gè)單純的列式存儲(chǔ)格式,仍然是首先根據(jù)行組分割整個(gè)表挪蹭,在每一個(gè)行組內(nèi)進(jìn)行按列存儲(chǔ)亭饵。ORC文件是自描述的,它的元數(shù)據(jù)使用Protocol Buffers序列化梁厉,并且文件中的數(shù)據(jù)盡可能的壓縮以降低存儲(chǔ)空間的消耗辜羊,目前也被Spark SQL、Presto等查詢引擎支持词顾,但是Impala對(duì)于ORC目前沒(méi)有支持八秃,仍然使用Parquet作為主要的列式存儲(chǔ)格式(可能parquet是cloudera自家開發(fā)的吧,競(jìng)品ORC架構(gòu)和parquet又相似度很高)肉盹。
ORC文件結(jié)構(gòu)
和Parquet類似昔驱,ORC文件也是以二進(jìn)制方式存儲(chǔ)的,所以是不可以直接讀取上忍,ORC文件也是自解析的骤肛,它包含許多的元數(shù)據(jù),這些元數(shù)據(jù)都是同構(gòu)ProtoBuffer進(jìn)行序列化的窍蓝。ORC的文件結(jié)構(gòu)見下圖腋颠,其中涉及到如下的概念:
- ORC文件:保存在文件系統(tǒng)上的普通二進(jìn)制文件,一個(gè)ORC文件中可以包含多個(gè)stripe它抱,每一個(gè)stripe包含多條記錄秕豫,這些記錄按照列進(jìn)行獨(dú)立存儲(chǔ),對(duì)應(yīng)到Parquet中的row group的概念观蓄。
- 文件級(jí)元數(shù)據(jù):包括文件的描述信息PostScript混移、文件meta信息(包括整個(gè)文件的統(tǒng)計(jì)信息)、所有stripe的信息和文件schema信息侮穿。
- stripe:一組行形成一個(gè)stripe歌径,每次讀取文件是以行組為單位的,一般為HDFS的塊大小亲茅,保存了每一列的索引和數(shù)據(jù)回铛。
- stripe元數(shù)據(jù):保存stripe的位置、每一個(gè)列的在該stripe的統(tǒng)計(jì)信息以及所有的stream類型和位置克锣。
- row group:索引的最小單位茵肃,一個(gè)stripe中包含多個(gè)row group,默認(rèn)為10000個(gè)值組成袭祟。
- stream:一個(gè)stream表示文件中一段有效的數(shù)據(jù)验残,包括索引和數(shù)據(jù)兩類。索引stream保存每一個(gè)row group的位置和統(tǒng)計(jì)信息巾乳,數(shù)據(jù)stream包括多種類型的數(shù)據(jù)您没,具體需要哪幾種是由該列類型和編碼方式?jīng)Q定鸟召。
從ORC數(shù)據(jù)格式來(lái)看,發(fā)現(xiàn)其結(jié)構(gòu)和parquet很類似氨鹏,也是先分行欧募,每個(gè)row group里面,對(duì)數(shù)據(jù)進(jìn)行列式存儲(chǔ)仆抵,然后提供元數(shù)據(jù)(包括每個(gè)column的offset跟继,字典表,壓縮算法等等)镣丑。在parquet里面我談到还栓,如果想往parquet追加一列數(shù)據(jù)的話,需要往每個(gè)row group里面添加列传轰,需要打破原有的column chunk,并且重新更新footer元數(shù)據(jù)信息谷婆。而ORC也類似慨蛙,需要增添的列打攤到每個(gè)stripe,在row data里面添加一列纪挎,更新index和stream期贫。
因?yàn)镠DFS是一次寫的文件系統(tǒng),ORC是一次寫的文件格式异袄,因此ORC為了支持ACID操作通砍,采用基礎(chǔ)文件和增量文件來(lái)實(shí)現(xiàn)insert,update烤蜕,delete封孙。大致思想是transaction會(huì)被存儲(chǔ)在增量文件中,并且當(dāng)delte變多會(huì)自動(dòng)合并讽营,當(dāng)查詢數(shù)據(jù)時(shí)虎忌,將原數(shù)據(jù)與delta數(shù)據(jù)排序,然后取最近的更改橱鹏。詳情見ACID膜蠢。