直接使用 dplyr 改变数据库表中的变量

Posted

技术标签:

【中文标题】直接使用 dplyr 改变数据库表中的变量【英文标题】:Mutate variables in database tables directly using dplyr 【发布时间】:2018-06-08 18:59:19 【问题描述】:

这是 MonetDBLite 数据库文件中的 mtcars 数据。

library(MonetDBLite)
library(tidyverse)
library(DBI)

dbdir <- getwd()
con <- dbConnect(MonetDBLite::MonetDBLite(), dbdir)

dbWriteTable(conn = con, name = "mtcars_1", value = mtcars)

data_mt <- con %>% tbl("mtcars_1")

我想使用 dplyr mutate 创建新变量并将其添加(提交!)到数据库表中?像

data_mt %>% select(mpg, cyl) %>% mutate(var = mpg/cyl) %>% dbCommit(con)

我们想要的输出应该是一样的:

dbSendQuery(con, "ALTER TABLE mtcars_1 ADD COLUMN var DOUBLE PRECISION")
dbSendQuery(con, "UPDATE mtcars_1 SET var=mpg/cyl") 

怎么做?

【问题讨论】:

dplyr 永远不会改变数据源,这是设计使然。我没有看到 dbplyr 的方法,但也许还有其他包可以实现这一点? @krlmlr 也许您会对如何解决答案的 cmets 中讨论的问题有所了解? 如果您拆分问题可能会有所帮助: 1. 创建一个“更新表/视图”,其中包含目标表的主键和新/更改的列;在这里,您可以使用 dbplyr 的全部功能。 2. 在更简单的UPDATE &lt;target_table&gt;, &lt;update_view&gt; SET ... WHERE &lt;join_expression&gt;UPDATE &lt;target_table&gt; SET ... JOIN &lt;update_view&gt; 中使用“更新视图”。 -- “更新视图”可以是一个复杂的 SQL 表达式,确切的 SQL 语法可能在 DBMS 之间有所不同。 【参考方案1】:

这里有几个函数,createupdate.tbl_lazy

它们分别实现了CREATE TABLE,这很简单,而ALTER TABLE/UPDATE 对则不那么简单:

创建

create <- function(data,name)
  DBI::dbSendQuery(data$src$con,
                   paste("CREATE TABLE", name,"AS", dbplyr::sql_render(data)))
  dplyr::tbl(data$src$con,name)

示例:

library(dbplyr)
library(DBI)
con <- DBI::dbConnect(RSQLite::SQLite(), path = ":memory:")
copy_to(con, head(iris,3),"iris")

tbl(con,"iris") %>% mutate(Sepal.Area= Sepal.Length * Sepal.Width) %>% create("iris_2")

# # Source:   table<iris_2> [?? x 6]
# # Database: sqlite 3.22.0 []
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Area
#          <dbl>       <dbl>        <dbl>       <dbl> <chr>        <dbl>
# 1          5.1         3.5          1.4         0.2 setosa        17.8
# 2          4.9         3            1.4         0.2 setosa        14.7
# 3          4.7         3.2          1.3         0.2 setosa        15.0

更新

update.tbl_lazy <- function(.data,...,new_type="DOUBLE PRECISION")
  quos <- rlang::quos(...)
  dots <- rlang::exprs_auto_name(quos, printer = tidy_text)

  # extract key parameters from query
  sql <- dbplyr::sql_render(.data)
  con  <- .data$src$con
  table_name <-gsub(".*?(FROM (`|\")(.+?)(`|\")).*","\\3",sql)
  if(grepl("\nWHERE ",sql)) where <-  regmatches(sql, regexpr("WHERE .*",sql))
  else where <- ""
  new_cols <- setdiff(names(dots),colnames(.data))

  # Add empty columns to base table
  if(length(new_cols))
    alter_queries <- paste("ALTER TABLE",table_name,"ADD COLUMN",new_cols,new_type)
    purrr::walk(alter_queries, ~
      rs <- DBI::dbSendStatement(con, .)
      DBI::dbClearResult(rs))

  # translate unevaluated dot arguments to SQL instructions as character
  translations  <- purrr::map_chr(dots, ~ translate_sql(!!! .))
  # messy hack to make translations work
  translations <- gsub("OVER \\(\\)","",translations) 

  # 2 possibilities: called group_by or (called filter or called nothing)
  if(identical(.data$ops$name,"group_by"))
    # ERROR if `filter` and `group_by` both used
    if(where != "") stop("Using both `filter` and `group by` is not supported")

    # Build aggregated table
    gb_cols   <- paste0('"',.data$ops$dots,'"',collapse=", ")
    gb_query0 <- paste(translations,"AS", names(dots),collapse=", ")
    gb_query  <- paste("CREATE TABLE TEMP_GB_TABLE AS SELECT",
                       gb_cols,", ",gb_query0,
                       "FROM", table_name,"GROUP BY", gb_cols)
    rs <- DBI::dbSendStatement(con, gb_query)
    DBI::dbClearResult(rs)

    # Delete temp table on exit
    on.exit(
      rs <- DBI::dbSendStatement(con,"DROP TABLE TEMP_GB_TABLE")
      DBI::dbClearResult(rs)
    )

    # Build update query
    gb_on <- paste0(table_name,'."',.data$ops$dots,'" = TEMP_GB_TABLE."', .data$ops$dots,'"',collapse=" AND ")
    update_query0 <- paste0(names(dots)," = (SELECT ", names(dots), " FROM TEMP_GB_TABLE WHERE ",gb_on,")",
                            collapse=", ")
    update_query <- paste("UPDATE", table_name, "SET", update_query0)
    rs <- DBI::dbSendStatement(con, update_query)
    DBI::dbClearResult(rs)

   else 

    # Build update query in case of no group_by and optional where
    update_query0 <- paste(names(dots),'=',translations,collapse=", ")
    update_query  <- paste("UPDATE", table_name,"SET", update_query0,where)
    rs <- DBI::dbSendStatement(con, update_query)
    DBI::dbClearResult(rs)
  
  tbl(con,table_name)

示例 1,定义 2 个新的数字列:

tbl(con,"iris") %>% update(x=pmax(Sepal.Length,Sepal.Width),
                           y=pmin(Sepal.Length,Sepal.Width))

# # Source:   table<iris> [?? x 7]
# # Database: sqlite 3.22.0 []
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species     x     y
#          <dbl>       <dbl>        <dbl>       <dbl> <chr>   <dbl> <dbl>
# 1          5.1         3.5          1.4         0.2 setosa    5.1   3.5
# 2          4.9         3            1.4         0.2 setosa    4.9   3  
# 3          4.7         3.2          1.3         0.2 setosa    4.7   3.2

示例 2,修改现有列,创建 2 个不同类型的新列:

tbl(con,"iris") %>%
  update(x= Sepal.Length*Sepal.Width,
         z= 2*y,
         a= Species %||% Species,               
         new_type = c("DOUBLE","VARCHAR(255)"))

# # Source:   table<iris> [?? x 9]
# # Database: sqlite 3.22.0 []
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species     x     y     z a           
#          <dbl>       <dbl>        <dbl>       <dbl> <chr>   <dbl> <dbl> <dbl> <chr>       
# 1          5.1         3.5          1.4         0.2 setosa   17.8   3.5   7   setosasetosa
# 2          4.9         3            1.4         0.2 setosa   14.7   3     6   setosasetosa
# 3          4.7         3.2          1.3         0.2 setosa   15.0   3.2   6.4 setosasetosa

示例 3,更新位置:

tbl(con,"iris") %>% filter(Sepal.Width > 3) %>% update(a="foo")

# # Source:   table<iris> [?? x 9]
# # Database: sqlite 3.22.0 []
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species     x     y     z a           
#          <dbl>       <dbl>        <dbl>       <dbl> <chr>   <dbl> <dbl> <dbl> <chr>       
# 1          5.1         3.5          1.4         0.2 setosa   17.8   3.5   7   foo         
# 2          4.9         3            1.4         0.2 setosa   14.7   3     6   setosasetosa
# 3          4.7         3.2          1.3         0.2 setosa   15.0   3.2   6.4 foo

示例 4:按组更新

tbl(con,"iris") %>%
  group_by(Species, Petal.Width) %>%
  update(new_col1 = sum(Sepal.Width,na.rm=TRUE), # using a R function
         new_col2 = MAX(Sepal.Length))           # using native SQL

# # Source:   SQL [?? x 11]
# # Database: sqlite 3.22.0 []
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species        x     y     z a            new_col1 new_col2
#          <dbl>       <dbl>        <dbl>       <dbl> <chr>      <dbl> <dbl> <dbl> <chr>           <dbl>    <dbl>
# 1          5.1         3.5          1.4         0.2 setosa         1     2   7   foo               6.5      5.1
# 2          4.9         3            1.4         0.2 setosa         1     2   6   setosasetosa      6.5      5.1
# 3          7           3.2          4.7         1.4 versicolor     1     2   6.4 foo               3.2      7 

一般说明

代码使用 dbplyr::translate_sql,因此我们可以使用 R 函数或原生函数,就像在古老的 mutate 调用中一样。

update 只能在一个filter 调用或一个group_by 调用或每个调用为零之后使用,否则会出现错误或意外结果。

group_by 的实现非常 hacky,因此没有空间来动态定义列或按操作分组,坚持基本原则。

updatecreate 都返回 tbl(con, table_name),这意味着您可以根据需要链接任意数量的 createupdate 调用,并使用适当数量的 group_byfilter介于两者之间。事实上,我的所有 4 个示例都可以链接。

要敲钉子,create 不会受到同样的限制,在调用它之前,您可以尽情享受dbplyr 的乐趣。

我没有实现类型检测,所以我需要new_type 参数,它在我的代码中alter_queries 定义的paste 调用中被回收,因此它可以是单个值或向量。

解决后者的一种方法是从translations 变量中提取变量,在dbGetQuery(con,"PRAGMA table_info(iris)") 中找到它们的类型。然后我们需要所有现有类型之间的强制规则,我们已经设置好了。但是由于不同的DBMS有不同的类型,我想不出一个通用的方法,我也不知道MonetDBLite

【讨论】:

有趣的解决方案!谢谢!我拥有的数据库文件是 50GB。因此,ALTER 和 UPDATE、create 函数和 mutate 的组合将是惊人的。 我深入研究了它,我认为我更新的解决方案应该很适合你。 @Moody_Mudskipper,这太棒了!我是否也可以对 group_by 使用更新函数 mutate mean 比如 "tbl(con,"iris") %>% group_by(Species, Petal.Length) %>% mutate(b = mean(Sepal.Length, na.rm = T ))" 对应的 SQL 代码是什么?我试图写它,但没有得到任何地方 总结一下,show_query 给出,SELECT Species, Petal.Length, AVG(Sepal.Length) AS b FROM iris GROUP BY Species, Petal.Length。不知道如何使用它来改变值!

以上是关于直接使用 dplyr 改变数据库表中的变量的主要内容,如果未能解决你的问题,请参考以下文章

在循环中使用胶水和 dplyr 获取关卡名称

在 dplyr mutate_at 调用中使用多列的函数

Oracle过程 - 选择所有表,然后循环遍历这些表的记录

R:如何使用 dplyr(函数 scr_postgres)从 redshift 中的模式中选择表?

Microsoft Access 2013 - 表数据中的变量

R 在使用 dplyr 或 RPostgreSQL 的模式下访问 redshift 表