Apache Parquet 是流行列存储文件格式,在Hadoop生态中被广泛使用,如Pig, Spark, 和 Hive。它是独立于语言的二进制格式,扩展名为.parquet,用于高效大数据集存储。本文主要介绍parquet格式如何实现高效数据存储。
列存储
parquet特征
- 跨平台,很多系统支持并使用的文件格式
- 按列布局存储数据,存储元数据
后者支持有效存储、查询数据。
假设有下列数据:
tibble::tibble(id = 1:3,
name = c("n1", "n2", "n3"),
age = c(20, 35, 62))
# A tibble: 3 x 3
# id name age
# <int> <chr> <dbl>
#1 1 n1 20
#2 2 n2 35
#3 3 n3 62
如果存储为CSV文件,在R终端中看到的是文件存储格式的镜像,为行存储,可有效实现文件查询,如:
SELECT * FROM table_name WHERE id == 2
只要定位到第二行并返回数据,另外追加行到数据集也很方便,仅需要在文件结尾增加一行。但如果想汇总age列数据,那么可能是低效的,因为需要变量每一行并确定那个值是age,再返回。
parquet使用列存储,按列布局,列数据按顺序存储。
1 2 3
n1 n2 n3
20 35 62
使用该布局,执行下面查询也是不方便,但如果需要汇总所有age,则仅需要简单汇总第三行。
SELECT * FROM dd WHERE id == 2
读写parquet文件
在R中,读写parquet文件需要使用arrow
包:
# install.packages("arrow")
library("arrow")
packageVersion("arrow")
#> [1] '6.0.1'
创建parquet文件,需要使用write_parquet()函数:
# Use the penguins data set
data(penguins, package = "palmerpenguins")
# Create a temporary file for the output
parquet = tempfile(fileext = ".parquet")
write_parquet(penguins, sink = parquet)
读文件使用read_parquet()
。使用parquet文件格式其中一个优势是文件占用控件较小,当考虑云存储成本时,应对大数据集时该优势尤为重要。
减少文件大小可通过两种方法实现:
- 文件压缩:可以通过给write_parquet()方法的参数compression设置压缩算法,默认为snappy。
- 巧妙的输出存储方式,即编码方式。
Parquet 编码
既然parquet使用列存储,则相同类型值被存储在一起。这打开了优化技巧的世界,当数据按行存储时是不支持的,例如CSV文件。下面介绍列存储中常用的编码。
行程编码(Run length encoding)
假设列仅包含单个值(每行数据一样),则可以仅存储"值重复N次",而不是像CSV格式中重复存储相同的值。这意味着当N很大时,存储所需空间很小。如果列有多个值,我们可以使用查找表。在parquet中这称为行程编码,列如下面列:
c(4, 4, 4, 4, 4, 1, 2, 2, 2, 2)
#> [1] 4 4 4 4 4 1 2 2 2 2
则会存储:
- 4 重复5次
- 1 重复1次
- 2 重复4次
下面通过简单示例演示该过程,字符A在数据框中重复多次:
x = data.frame(x = rep("A", 1e6))
然后分别保存为parquet和csv格式进行对比:
library(arrow)
parquet = tempfile(fileext = ".parquet")
csv = tempfile(fileext = ".csv")
arrow::write_parquet(x, sink = parquet, compression = "uncompressed")
readr::write_csv(x, file = csv)
使用fs包进行对比:
# Could also use file.info()
fs::file_info(c(parquet, csv))[, "size"]
#> # A tibble: 2 × 1
#> size
#> <fs::bytes>
#> 1 1014
#> 2 1.91M
结果显示parquet文件很小,CSV文件将近2M,文件空间减少了500倍。
字典编码(Dictionary encoding)
假设存储下面字符向量:
c("Jumping Rivers", "Jumping Rivers", "Jumping Rivers")
#> [1] "Jumping Rivers" "Jumping Rivers" "Jumping Rivers"
保存上面数据,可以简单使用0代替"Jumping Rivers",使用map映射两者关系,可以有效减少存储空间,特别对于大向量。
x = data.frame(x = rep("Jumping Rivers", 1e6))
arrow::write_parquet(x, sink = parquet)
readr::write_csv(x, file = csv)
fs::file_info(c(parquet, csv))[, "size"]
#> # A tibble: 2 × 1
#> size
#> <fs::bytes>
#> 1 1.09K
#> 2 14.31M
增量编码(Delta encoding)
增量编码典型用于连续时间戳。时间一般使用Unix时间表示,即从1970-01-01开始的秒数,存储方式对于人类不友好,一般需要格式化之后再展示,举例:
(time = Sys.time())
#> [1] "2022-03-16 17:47:36 GMT"
unclass(time)
#> [1] 1647452856
如果列有大量时间戳数据,为了减少存储空间,可以给所有列减去列中的最小值,举例:
c(1628426074, 1628426078, 1628426080)
#> [1] 1628426074 1628426078 1628426080
采用增量,仅需要这样存储,以1628426074为基数的偏移量:
c(0, 4, 6)
#> [1] 0 4 6
其他编码
RDS是R语言支持的文件格式,使用readRDS()/saveRDS() 和 load()/save()函数进行读写。RDS主要优势能够存储R对象——环境、list、函数等。如果仅对矩形数据框,如data frame,那么使用RDS的理由:
- 文件格式已经存在很久,不会可能改变,因此向后兼容
- 不依赖任何外部包,仅Base R
使用parquet优势为:
- 文件大小相对较小,如果需要压缩,可在write_parquet()中设置compression = “gzip”。一般来说parquet文件节省空间,对一些场景,节省%5也非常值得。