SQL 将许多 tsv 文件连接到数据库中的单个表中,同时跟踪文件源 (MonetDBLite)

Posted

技术标签:

【中文标题】SQL 将许多 tsv 文件连接到数据库中的单个表中,同时跟踪文件源 (MonetDBLite)【英文标题】:SQL concatenate many tsv files into single table in a database, while keeping track of file source (MonetDBLite) 【发布时间】:2017-05-15 15:05:22 【问题描述】:

我正在使用 MonetDBLite R 包创建 MonetDB。我可以使用here 中的说明创建数据库表,代码如下:

library(DBI)
library(MonetDBLite)

# Write tsv file of mtcars
write.table(mtcars, "mtcars.tsv", row.names=FALSE, sep= "\t")

# Initialize MonetDB
dbdir <- "/Users/admin/my_directory"
con <- dbConnect(MonetDBLite::MonetDBLite(), dbdir)

# Write table
dbWriteTable(con, "test4", "mtcars.tsv", delim="\t")

下面的查询给出了

> dbGetQuery(con, "SELECT * FROM test4 LIMIT 3")
mpg cyl  disp  hp drat    wt  qsec vs am gear carb
1  21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
2  21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
3  22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1

到目前为止一切顺利。但是,假设我有另一个文件 mtcars2 具有不同的 mpg 值:

mtcars2 <- mtcars
mtcars2$mpg <- mtcars2$mpg + 5
write.table(mtcars2, "mtcars2.tsv", row.names= FALSE, sep = "\t")

我可以将它加载到另一个表:

dbWriteTable(con, "test5", "mtcars2.tsv", delim = "\t")
> dbGetQuery(con, "SELECT * FROM test5 LIMIT 3")
mpg cyl disp  hp drat    wt  qsec vs am gear carb
1 26.0   6  160 110 3.90 2.620 16.46  0  1    4    4
2 26.0   6  160 110 3.90 2.875 17.02  0  1    4    4
3 27.8   4  108  93 3.85 2.320 18.61  1  1    4    1

也不错。但我的问题是:我想稍后查找所有具有 6 个cyl 的汽车的mpg,并知道它来自哪个数据集(mtcars 或 mtcars2)。根据我对 SQL 索引的了解(这不是很多,基本上是我读过的 here),我应该将所有数据放在一个表中以进行最有效的搜索。我尝试加载第一个 tsv 文件,然后使用 ALTER TABLE test4 ADD dataset TEXTUPDATE test4 SET dataset = dataset1 sql 命令添加另一列 -

dbSendQuery(con, "UPDATE test4 SET dataset = dataset1")
dbSendQuery(con, "UPDATE test4 SET dataset = 1")
> dbGetQuery(con, "SELECT * FROM test4 LIMIT 3")
mpg cyl disp  hp drat    wt  qsec vs am gear carb dataset
1 21.0   6  160 110 3.90 2.620 16.46  0  1    4    4       1
2 21.0   6  160 110 3.90 2.875 17.02  0  1    4    4       1
3 22.8   4  108  93 3.85 2.320 18.61  1  1    4    1       1

但是当我尝试将 mtcars2 附加到表中时,它有不同数量的列(正如我应该预料的那样,duh)。将许多具有相同列的 tsv 文件中的数据连接到一个表中,同时跟踪数据源的最佳方法是什么?

编辑——你可能已经猜到了,真正的数据不是 mtcars——它是数百万行长的扁平 tsv 文件,这意味着我想避免将整个文件读入内存并使用 R 进行操作。

【问题讨论】:

逻辑过程:导入两个数据集后,更改每个表以定义source 字段。然后将该字段更新为两个表中所有记录的所需值。然后从一个表中选择,将所有需要的记录插入到第二个表中。然后删除第二个不需要/合并的表。或者编写一个合并两个表并添加列的视图;但它不会像你已经注意到的那么有效.. 太棒了!这是有道理的——如果我能设法让它以这种方式工作,我会发布代码。 【参考方案1】:

按照xQbert 的建议,我仅使用 SQL 命令解决了(考虑到我的数据是 10 多个文件,每百万行长,这比 bash 命令是必要且更快的)。

library(DBI)
library(MonetDBLite)

# Write tsv file of mtcars
write.table(mtcars, "mtcars.tsv", row.names=FALSE, sep= "\t")

# Write tsv of second mtcars
mtcars2 <- mtcars
mtcars2$mpg <- mtcars2$mpg + 5
write.table(mtcars2, "mtcars2.tsv", row.names= FALSE, sep = "\t")

# Initialize MonetDB
dbdir <- "/Users/admin/"
con <- dbConnect(MonetDBLite::MonetDBLite(), dbdir)

# Write table
dbWriteTable(con, "test4", "mtcars.tsv", delim="\t")

# Add data source information
dbSendQuery(con, "ALTER TABLE test4 ADD source TEXT")
dbSendQuery(con, "UPDATE test4 SET source = 'dataset1'")

# Write second dataset to a temporary table
dbWriteTable(con, "temptable", "mtcars2.tsv", delim="\t")

# Add data source information
dbSendQuery(con, "ALTER TABLE temptable ADD source TEXT")
dbSendQuery(con, "UPDATE temptable SET source = 'dataset2'")

# Insert temp table into main table
dbSendQuery(con, "INSERT INTO test4 SELECT * FROM temptable")

# Drop temp table
dbSendQuery(con, "DROP TABLE temptable")

# Checking the data, truncated for clarity
> dbGetQuery(con, "SELECT * FROM test4")
mpg cyl  disp  hp drat    wt  qsec vs am gear carb   source
1  21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4 dataset1
2  21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4 dataset1
3  22.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1 dataset1
...
33 26.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4 dataset2
34 26.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4 dataset2
35 27.8   4 108.0  93 3.85 2.320 18.61  1  1    4    1 dataset2
...
64 26.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2 dataset2

很抱歉,如果我没有在我的数据比 mtcars 大得多的问题中说得足够清楚 - 如果您有中等大小的数据,data.tables 包可能是比数据库更好的解决方案。

【讨论】:

是的!处理 32 个文件、490 万行、11 列大约需要 15 分钟。这可能和我要进入 R 框架一样好。谢谢! 优秀。一定要采纳你的答案! :P 我将源命名为文件名,但那是我......或者你可能想要混淆。 啊哈这是未发表的数据,我可能太偏执了,但不想在研究中被抢先一步。 Stack Overflow 说我需要等到明天才能接受我自己的答案,但我一定会这样做。【参考方案2】:

您应该能够在读取文件后执行dbWriteTable() 来做您想做的事情,在data.frame 中创建一个新变量。比如:

  library(DBI)
  library(MonetDBLite)
  library(data.table)

  # Write tsv file of mtcars
  tmp <- tempfile()
  write.table(mtcars, tmp, row.names=FALSE, sep= "\t")

  # Initialize MonetDB
  dbdir <- "~/Desktop/temp"
  con <- dbConnect(MonetDBLite::MonetDBLite(), dbdir)

  test4df <- fread(tmp)
  test4df$dataset <- 1
  dbWriteTable(con, "test4", test4df)

  dbReadTable(con, "test4")

  test5df <- fread(tmp)
  test5df$mpg <- test5df$mpg + 5
  test5df$dataset <- 2
  dbWriteTable(con, "test4", test5df, append = TRUE)

  dbReadTable(con, "test4")

编辑(不打开文件中途建议)

如果您想在不打开文件的情况下进行工作,您可以执行以下操作来修改文件并附加另一个字段。正如我所写,这将适用于带有bash 的操作系统。

infile <- tmp
outfile <- tempfile()

# open connections
incon <- file(description = infile, open = "r")
outcon <- file(description = outfile, open = "w")

# count the number of lines (this will work only with Mac/Linux)
com <- paste("wc -l ", infile, " | awk ' print $1 '", sep="")
n <- system(command=com, intern=TRUE)

# work with the first line
txt <- scan(file = incon, what = character(), nlines=1, quiet=TRUE)
txt <- c(txt, "dataset")
cat(paste(txt, collapse = "\t"), "\n", file = outcon, sep = "")

# work with the rest of the file
for(i in 2:n) 
  txt <- scan(file = incon, what = character(), nlines=1, quiet=TRUE)
  txt <- c(txt, "1")
  cat(paste(txt, collapse = "\t"), "\n", file = outcon, sep = "")

close(incon);close(outcon)
dbWriteTable(con, "test4", outfile, delim = "\t")
# do the similar for other files

【讨论】:

是的,对于 mtcars 大小的数据,这是最好的解决方案(如果您出于某种原因需要将其设为数据库)。但我的真实数据不是 mtcars 大小的——它是数百万行长的文本文件,这意味着我们希望尽可能避免将整个文件读入内存。 我编辑了我的答案。首先,我会尝试在data.table 中使用fread,这将提高内存效率。如果这不起作用,我会尝试第二种可能的解决方案。 不幸的是,我们要走MonetDBlite 路线,因为fread 对于我们正在分析的文件的数量和大小来说太慢了。不过感谢您的建议,data.tables 包非常适合特定大小的数据。 请仔细阅读我的评论/回答。正如我在上一条评论中所说,我在编辑后的答案中显示的建议不依赖于data.table,而是在不打开文件的情况下在数据库中写入表。 是的,我还看到您在编辑的评论中使用了 bash 命令。由于我们希望将其写入跨平台的代码,因此使用 SQL 命令比使用 bash 命令更好。据我了解,它也比使用 SQL 命令慢,但我可能错了。【参考方案3】:

这就是我要做的,给定一组具有相同结构和文件名的文件,最终表中所需的文件名,否则就是所有文件中数据的组合:

# say we have those files
write.table(mtcars, "mtcars1.tsv", row.names=FALSE, sep= "\t")
write.table(mtcars, "mtcars2.tsv", row.names=FALSE, sep= "\t")

# write them individually, and add a column that contains the file name
dbWriteTable(con, "mtcars1", "mtcars1.tsv", delim="\t")
dbSendQuery(con, "ALTER TABLE mtcars1 ADD COLUMN file STRING DEFAULT 'mtcars1.tsv';")
dbWriteTable(con, "mtcars2", "mtcars2.tsv", delim="\t")
dbSendQuery(con, "ALTER TABLE mtcars2 ADD COLUMN file STRING DEFAULT 'mtcars2.tsv';")

# now combine into a new table
dbSendQuery(con, "CREATE TABLE mtcars_mat AS SELECT * FROM mtcars1 UNION ALL SELECT * FROM mtcars2")

# or a view if you don't need to modify the data in the mtcars table (faster)
dbSendQuery(con, "CREATE view mtcars AS SELECT * FROM mtcars1 UNION ALL SELECT * FROM mtcars2")



# and here is the same as a loop with a filename glob and some added robustness (handy if you have 1000 files)
files <- Sys.glob("/some/path/mtcars*.tsv")
tables <- dbQuoteIdentifier(con, tools::file_path_sans_ext(basename(files)))
dbBegin(con)
for (i in 1:length(files)) 
  dbWriteTable(con, tables[i], files[i], delim="\t", transaction=FALSE)
  dbSendQuery(con, paste0("ALTER TABLE ", tables[i], " ADD COLUMN file STRING DEFAULT ",dbQuoteString(con, files[i]),";"))

dbSendQuery(con, paste0("CREATE TABLE somefinalresult AS ", paste0("SELECT * FROM ",tables, collapse=" UNION ALL ")))
# remove the parts again, optional
dbSendQuery(con, paste0("DROP TABLE ", tables, ";", collapse=" "))
dbCommit(con)

【讨论】:

以上是关于SQL 将许多 tsv 文件连接到数据库中的单个表中,同时跟踪文件源 (MonetDBLite)的主要内容,如果未能解决你的问题,请参考以下文章

如何在单个查询中将 Advantage ADT 表连接到 DBF 表?

多个 MS Access 前端连接到单个远程 SQL Server 后端

通过 ODBC 将 pandas 输出连接到 Excel 工作表

我可以将MS Access连接到SQL Server而不是特定的SQL Server数据库吗?

从 R 运行 .sql 文件

SQL 内连接到左连接表