DuckDB 有几个很棒的特性,包括导入和导出 CSV 和 parquet 格式的能力天生的不影响 R 内存。
长话短说
con <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
DBI::dbExecute(con, "copy (select * from read_csv_auto('quux.csv', SAMPLE_SIZE=-1)) to 'quux3.pq' (format parquet)")
仅此而已。数据永远不会导入到 R 中。(现在,duckdb 是否可以在不耗尽内存的情况下自行完成是另一个我没有在本地验证的问题......)
买者自负: 但是,在您盲目相信这一点之前,我强烈建议您对类进行一些验证。其中大部分可以使用 duckdb 以“懒惰”的方式轻松完成,而无需将整个框架加载到 R 中。我鼓励您阅读更多关于原生查询 CSV/parquet 文件(无需加载到 R 中)的文档。
方法
为了比较这两种方法(通过你不想做的data.frame,以及通过duckdb),我们将使用“RSS”(来自ps::ps_memory_info())来指示当前的 R 进程内存用法。来自?ps::ps_memory_info:
* 'rss': "Resident Set Size", this is the non-swapped physical
memory a process has used (bytes). On UNIX it matches "top"‘s
'RES' column (see doc). On Windows this is an alias for
'wset' field and it matches "Memory" column of 'taskmgr.exe'.
虽然对 R 的真实影响的衡量并不完美,但它确实表明在使用 DuckDB 时对 R 的影响要小得多。
此外,每个方法都是在 R --vanilla 的新实例中完成的。未加载 .Rprofile 或站点初始化文件。你看到的代码就是执行的代码,仅此而已。
在 R 中通过 data.frame
Sys.getpid()
# [1] 20860
file.info("quux.csv")["size"] / (1024^2) # MBs
# size
# quux.csv 299.3079
mem1 <- ps::ps_memory_info()["rss"]
dat <- read.csv("quux.csv")
mem2 <- ps::ps_memory_info()["rss"]
arrow::write_parquet(dat, "quux1.pq")
mem3 <- ps::ps_memory_info()["rss"]
c(mem1, mem2, mem3, diff = mem3 - mem1) / (1024^2)
# rss rss rss diff.rss
# 57.70703 1218.55859 1548.54688 1490.83984
这表示 R 为 1490MB更大阅读完整数据后。 (仅供参考,data.table::fread 而不是 read.csv 结果只有 408MB 的内存增益,同样严峻的条件。不过我并没有尝试优化这部分:-)
(仅供参考,这些数字因每次运行而异,并且可能会根据此答案范围之外的其他因素而有所不同。我的笔记本电脑有 64GB RAM,它可能无法与您看到的完全相提并论。)
DuckDB,从 CSV 读取,写入 parquet
Sys.getpid()
# [1] 32485
mem1 <- ps::ps_memory_info()["rss"]
con <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
DBI::dbExecute(con, "copy (select * from read_csv_auto('quux.csv')) to 'quux2.pq' (format parquet)")
# [1] 1000207
mem2 <- ps::ps_memory_info()["rss"]
c(mem1, mem2, diff=mem2 - mem1) / (1024^2)
# rss rss diff.rss
# 63.23828 86.35938 23.12109
在此过程中仅显示 23MB。
比较生成的文件。
file.info(list.files(pattern = "quux.*"))["size"] / (1024^2)
# size
# quux.csv 299.30786
# quux1.pq 51.69008
# quux2.pq 66.84857
较大的文件是由于下面提到的类中的差异。我的猜测是如果我们力量某些 character 列为 logical,则其文件大小可能会减小。
再深入一点看一下内容:
ds1 <- arrow::open_dataset("quux1.pq")
ds2 <- arrow::open_dataset("quux2.pq")
identical(names(ds1), names(ds2))
# [1] TRUE
data.frame(
ds1 = sapply(head(ds1, 1), function(z) class(z)[1]),
ds2 = sapply(head(ds2, 1), function(z) class(z)[1])
)
# ds1 ds2
# V1 character character
# V2 integer integer
# V3 character character
# V4 integer integer
# V5 logical character
# V6 integer integer
# V7 character POSIXct
# V8 logical character
# V9 numeric numeric
# V10 numeric numeric
# V11 numeric integer
# V12 integer integer
# V13 integer integer
# V14 integer integer
# V15 numeric numeric
# V16 integer integer
# V17 integer integer
# V18 numeric numeric
# V19 numeric numeric
# V20 logical character
# V21 numeric numeric
# V22 numeric numeric
# V23 numeric numeric
# V24 integer integer
# V25 logical character
# V26 integer integer
# V27 integer integer
# V28 integer integer
# V29 integer integer
# V30 logical character
# V31 logical character
# V32 numeric numeric
# V33 logical character
# V34 logical character
# V35 logical character
# V36 logical character
# V37 logical character
# V38 logical character
# V39 character POSIXct
# V40 logical character
# V41 logical character
# V42 numeric integer
# V43 logical character
# V44 logical character
# V45 logical character
# V46 logical character
# V47 numeric numeric
# V48 logical character
# V49 logical character
# V50 logical character
# V51 logical character
# V52 logical character
# V53 logical character
# V54 logical character
# V55 logical character
# V56 logical character
# V57 logical character
从中可以推断出一些有趣的事情:
- 两个字段是时间戳,duckdb方法正确识别,解析,存储为数字时间戳;因为我没有明确地告诉 R 列类,所以它们默认为
character;
-
ds1中的logical和ds2中的character的所有列均为空(抱歉,这是我的数据);它们是不同类的事实表明 duckdb 默认为类似字符串的空值而不是“位”,这对您来说可能是也可能不是一个因素;
- 只有两列被分类为
numeric-vs-integer; V11 是真正的整数,没关系;第二个 V42 表明用于区分 numeric 和 integer 的启发式方法遗漏了一些东西。包含任何小数部分的 V42 的第一行位于第 37159 行。
修复数据差异
V42 列表示我们需要非常清楚该 parquet 生成器的进出情况。我的猜测是它在“CSV 导入”步骤中,因此查看 CSV Loading 表明需要更改 SAMPLE_SIZE。虽然效率相对较低,但我将使用 -1 表示它需要查看列中的所有值以确定其类。是的,更慢,但也更安全。
这个假设的验证:
> str(DBI::dbGetQuery(con, "select * from read_csv_auto('quux.csv') limit 5")[c("V11","V42")])
'data.frame': 5 obs. of 2 variables:
$ V11: int 4407 4408 4408 4407 4408
$ V42: int 26 25 23 21 3
> str(DBI::dbGetQuery(con, "select * from read_csv_auto('quux.csv', SAMPLE_SIZE=-1) limit 5")[c("V11","V42")])
'data.frame': 5 obs. of 2 variables:
$ V11: int 4407 4408 4408 4407 4408
$ V42: num 26 25 23 21 3
确实,V11还是不错的,V42从int变成了num。
使用这个新参数重新运行后,
DBI::dbExecute(con, "copy (select * from read_csv_auto('quux.csv', SAMPLE_SIZE=-1)) to 'quux3.pq' (format parquet)")
离线验证确认所有值都是正确的。