SQL中具有不同列的行绑定表

时间:2018-12-16 20:06:53

标签: sql r join union

我有一个非常简单的请求:我想使用共享某些列名而不是所有列名的SQL“堆叠”或垂直组合表。

如果要在R中解决此问题,Hadley Wickham的dplyr软件包具有一个名为bind_rows()的不错的函数,该函数按行绑定不同的表,并在其中一个表中不存在列时强制使用NA值。

作为一个例子,假设我们有表'A':

a <- head(iris) %>% 
dplyr::mutate(., species_num = 1:nrow(.)) %>%
dplyr::select(., -Petal.Width)

enter image description here

表'B':

b <- tail(iris) %>%
dplyr::mutate(., species_num = 7:12)

enter image description here

请注意,表B包含额外的列Petal.Width

如上所述,dplyr软件包中的R函数bind_rows()将执行以下操作:

nice <- dplyr::bind_rows(a, b)

enter image description here

对吧?

好吧,我想在SQL中执行相同的操作,但是当列号和/或名称不同时UNION失败...

(SELECT *, FROM a)
UNION
(SELECT *, FROM b); 

enter image description here

现在,我意识到我可以在使用Petal.Width之前将a列简单地添加到表UNION中,但是我要解决的现实问题涉及30多个表,每个表都包含一些专栏,但不同程度的其他专栏...我的最终目标是使这一过程自动化。简而言之,我需要一个不需要我解决问题或手动将列添加到各个表的解决方案。

有什么想法吗?

2 个答案:

答案 0 :(得分:2)

尝试一下:

带有一些虚假数据的预备:

# con <- dbConnect(...)
DBI::dbWriteTable(con, "iris1", iris[1:3,-1])
DBI::dbWriteTable(con, "iris2", iris[4:6,-2])
DBI::dbWriteTable(con, "iris23", iris[7:9,-(2:3)])

设置字段名称列表:

list_of_tables <- c("iris1", "iris2", "iris23")
eachnames <- sapply(list_of_tables, function(a) DBI::dbQuoteIdentifier(con, DBI::dbListFields(con, a)), simplify = FALSE)
str(eachnames)
# List of 3
#  $ iris1 :Formal class 'SQL' [package "DBI"] with 1 slot
#   .. ..@ .Data: chr [1:4] "\"Sepal.Width\"" "\"Petal.Length\"" "\"Petal.Width\"" "\"Species\""
#  $ iris2 :Formal class 'SQL' [package "DBI"] with 1 slot
#   .. ..@ .Data: chr [1:4] "\"Sepal.Length\"" "\"Petal.Length\"" "\"Petal.Width\"" "\"Species\""
#  $ iris23:Formal class 'SQL' [package "DBI"] with 1 slot
#   .. ..@ .Data: chr [1:3] "\"Sepal.Length\"" "\"Petal.Width\"" "\"Species\""
allnames <- unique(unlist(eachnames, use.names=FALSE))
allnames
# [1] "\"Sepal.Width\""  "\"Petal.Length\"" "\"Petal.Width\""  "\"Species\""     
# [5] "\"Sepal.Length\""

我通常使用DBI::dbQuoteIdentifier有点防御性,尽管由于列名而特别要求(我使用的是postgres,但它不喜欢字段名中的转义符)。

可以使用以下方法创建字段名称列表,并用null as进行扩展:

list_of_fields <- lapply(eachnames, function(a) {
  paste(ifelse(allnames %in% a, allnames, paste("null as", allnames)), collapse = ", ")
})
str(list_of_fields)
# List of 3
#  $ iris1 : chr "\"Sepal.Width\", \"Petal.Length\", \"Petal.Width\", \"Species\", null as \"Sepal.Length\""
#  $ iris2 : chr "null as \"Sepal.Width\", \"Petal.Length\", \"Petal.Width\", \"Species\", \"Sepal.Length\""
#  $ iris23: chr "null as \"Sepal.Width\", null as \"Petal.Length\", \"Petal.Width\", \"Species\", \"Sepal.Length\""

如果您有更复杂的查询需求,那么这是一个好的开始。这是一个不进行其他过滤的查询:

qry <- paste(
  mapply(function(nm, flds) {
    paste("( select",
          paste(ifelse(allnames %in% flds, allnames, paste("null as", allnames)),
                collapse = ", "),
          "from", nm, ")")
  }, names(eachnames), eachnames),
  collapse = " union\n")
cat(qry)
# ( select "Sepal.Width", "Petal.Length", "Petal.Width", "Species", null as "Sepal.Length" from iris1 ) union
# ( select null as "Sepal.Width", "Petal.Length", "Petal.Width", "Species", "Sepal.Length" from iris2 ) union
# ( select null as "Sepal.Width", null as "Petal.Length", "Petal.Width", "Species", "Sepal.Length" from iris23 )
DBI::dbGetQuery(con, qry)
#   Sepal.Width Petal.Length Petal.Width Species Sepal.Length
# 1          NA          1.7         0.4  setosa          5.4
# 2          NA           NA         0.3  setosa          4.6
# 3          NA          1.5         0.2  setosa          4.6
# 4          NA          1.4         0.2  setosa          5.0
# 5         3.0          1.4         0.2  setosa           NA
# 6         3.2          1.3         0.2  setosa           NA
# 7          NA           NA         0.2  setosa          5.0
# 8          NA           NA         0.2  setosa          4.4
# 9         3.5          1.4         0.2  setosa           NA

许多DBA通常建议不要使用SELECT *,因此这是次要的好处。

答案 1 :(得分:0)

我今天早些时候使用@ r2evans提供的答案,提出了以下内容,这些内容适用于我的实际情况(有关可重复性,请参阅@ r2evans的非常详细的答案)。我确定apply方法比循环更有效,但这是我的开始。

for(i in 1:nrow(myTables)) {

  # Load drivers
  drv <- RPostgreSQL::postgresqlInitDriver()

  # Establish connection to the database
  con <- DBI::dbConnect(drv, host=creds[1], port=creds[2], dbname=creds[3], 
                        user=creds[4], password=creds[5])

  # Return column names and variable class
  tableColumns <- DBI::dbGetQuery(con, paste0("SELECT column_name, data_type ",
                                          "FROM   information_schema.columns ",
                                          "WHERE  table_name = '", 
                                           myTables[i,'table_name'], "' ",
                                          "ORDER  BY ordinal_position"))

  # Close out the database connection
  DBI::dbDisconnect(con)

  # Determine missing columnms
  colDiffs <- setdiff(desiredCols, tableColumns$column_name)


  if(length(colDiffs) > 0) {

    # Create SQL subqueries, complete with inclusion for missing columns
    myTables[i,'subquery'] <- paste0('(SELECT *, ',
                                      paste(paste0('NULL AS ', colDiffs, ', '), 
                                                    collapse = ''), "'",
                                      myTables[i,'table_name'], "' AS table ",
                                      "FROM schema.", myTables[i,'table_name'], 
                                      ")")

  } else {

    # Create SQL subqueries
    myTables[i,'subquery'] <- paste0("(SELECT *, '",
                                       myTables[i,'table_name'], "' AS table ",
                                      "FROM schema.", myTables[i,'table_name'], ")")
  }

}


# Create final query
finalQuery <- paste(myTables$subquery, collapse=" UNION ")