【问题标题】:Export a SQLite table to Apache parquet without creating a dataframe在不创建数据框的情况下将 SQLite 表导出到 Apache parquet
【发布时间】:2023-02-09 05:27:34
【问题描述】:

我有多个巨大的 CSV 文件,我必须根据 Apache Parquet 格式导出这些文件,并根据多个条件/键(= 列值)将它们拆分成较小的文件。据我了解,Apache arrow 是允许使用 Apache parquet 文件的 R 包。

我在一个共享的实验室环境中工作,鉴于 RAM 内存有限(与在同一环境中同时工作的用户数量相比),我们建议在本地 SQLite 数据库中创建我们的数据框,而不是将它们导入内存中(进入 RAM) .

以下伪代码显示了我如何将 CSV 文件导入本地 SQLite 数据库。在下面的代码中,我使用了sqldftidyverse 包。

input_file_path <- "D:/tmp/mydata.csv"
db_file_path <- "D:/tmp/db_tmp_sqlite.db"
unlink(db_file_path)
sqldf(str_c("attach '", db_file_path, "' as new"))
sqldf(read.csv.sql(
    file = input_file_path,
    sql = "
        create table mytable as
        select
            . . .
        from
            file
    ",
    `field.types` = list(
      . . .
    ),
    ##
    header = TRUE,
    sep = ",",
    eol = "\n",
    dbname = db_file_path,
    drv = "SQLite"
))

这按预期运行良好,我的表已创建,我可以运行所有必需的 SQL 查询,特别是添加补充变量(我的表中的列),这些变量稍后将用作将我的表导出为 Apache Parquet 格式的键。然而,基于Apache Arrow for R Cheatsheet,函数write_dataset允许基于Apache Parquet格式导出我的数据,需要一个数据框.

这正是我的问题,因为 R 中的数据框在内存中,而我之前解释的数据在 SQLite 本地数据库中。这意味着首先我必须做一个 SELECT 将整个数据导出到 RAM,就像

df <- sqldf("select * from mytable", dbname = ...)

只有这样我才能使用 write_dataset 和创建的 df 数据框作为它的第一个参数,以便根据 Apache Parquet 格式导出和拆分我的数据。但这不是我想做的。考虑到我们共享环境中现有的资源限制(内存不足),重点是将数据放在 SQLite 而不是内存 (RAM) 中。

无论如何,是否可以直接从 R 程序中的 SQLite 转换为 Apache Parquet,而无需在导出之前先将整个数据放入数据框中,或者我正在尝试做一些根本不可能的事情?

【问题讨论】:

    标签: r sqlite parquet sqldf apache-arrow


    【解决方案1】:

    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中的logicalds2中的character的所有列均为空(抱歉,这是我的数据);它们是不同类的事实表明 duckdb 默认为类似字符串的空值而不是“位”,这对您来说可能是也可能不是一个因素;
    • 只有两列被分类为numeric-vs-integerV11 是真正的整数,没关系;第二个 V42 表明用于区分 numericinteger 的启发式方法遗漏了一些东西。包含任何小数部分的 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还是不错的,V42int变成了num

    使用这个新参数重新运行后,

    DBI::dbExecute(con, "copy (select * from read_csv_auto('quux.csv', SAMPLE_SIZE=-1)) to 'quux3.pq' (format parquet)")
    

    离线验证确认所有值都是正确的。

    【讨论】:

      猜你喜欢
      • 2023-03-19
      • 2019-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-20
      • 2015-11-05
      • 2010-12-19
      • 2019-12-20
      相关资源
      最近更新 更多