在 R 中为 DBI 查询声明变量到 MS SQL

Declaring variable in R for DBI query to MS SQL

我正在编写一个 R 查询,其中 运行 有几个 SQL 查询使用 DBI 包来创建报告。为了完成这项工作,我需要能够在 R 中声明一个变量(例如 Period End Date),然后从 within SQL 查询中调用该变量。当我 运行 我的查询时,出现以下错误:

如果我只使用字段名称 (PeriodEndDate),我会收到以下错误:

Error in (function (classes, fdef, mtable) : unable to find an inherited method for function ‘dbGetQuery’ for signature ‘"Microsoft SQL Server", "character"’

如果我使用@访问字段名(@PeriodEndDate),我得到以下错误:

Error: nanodbc/nanodbc.cpp:1655: 42000: [Microsoft][ODBC SQL Server Driver][SQL Server]Must declare the scalar variable "@PeriodEndDate". [Microsoft][ODBC SQL Server Driver][SQL Server]Statement(s) could not be prepared. '

示例查询可能如下所示:

library(DBI)  # Used for connecting to SQL server and submitting SQL queries.
library(tidyverse)  # Used for data manipulation and creating/saving CSV files.
library(lubridate) # Used to calculate end of month, start of month in queries

# Define time periods for queries.
PeriodEndDate <<- ceiling_date(as.Date('2021-10-31'),'month')  # Enter Period End Date on this line.
PeriodStartDate <<- floor_date(PeriodEndDate, 'month')

# Connect to SQL Server.
con <- dbConnect(
  odbc::odbc(),
  driver = "SQL Server",
  server = "SERVERNAME",
  trusted_connection = TRUE,
  timeout = 5,
  encoding = "Latin1")

samplequery <- dbGetQuery(con, "
     SELECT * FROM [TableName]
     WHERE OrderDate <= @PeriodEndDate           
")

我认为一种方法可能是使用粘贴功能,如下所示:

samplequery <- dbGetQuery(con, paste("
     SELECT * FROM [TableName]
     WHERE OrderDate <=", PeriodEndDate")

但是,如果涉及 多个 变量在查询外或在查询内的多个位置被引用,这可能会变得笨拙。

有没有相对简单的方法来做到这一点?

提前感谢您的任何想法!

大多数基于 DBI 的连接中的机制是在查询中使用 ?-占位符[1],在中使用 params=调用 DBI::dbGetQueryDBI::dbExecute.

也许是这样:

samplequery <- dbGetQuery(con, "
     SELECT * FROM [TableName]
     WHERE OrderDate <= ?
", params = list(PeriodEndDate))

一般来说,https://db.rstudio.com/best-practices/run-queries-safely/ 中很好地列举了将 R 对象作为 data-item 包含的机制。按照我推荐的顺序,

  1. 参数化查询(如上图);
  2. glue::glue_sql;
  3. sqlInterpolate(使用与#1 相同的 ? 占位符);
  4. link 也提到了使用 dbQuoteString 的“手动转义”。

由于疏忽,我认为其他任何事情都比较冒险 SQL corruption/injection.

我在这里看到很多关于尝试使用以下技术之一的问题:paste and/or sprintf 使用 sQuote 或 hard-coded paste0("'", PeriodEndDate, "'")。这些在我看来太脆弱了,应该避免。

我对参数化查询的偏好超出了这种可用性,它还会对重复使用同一查询产生 non-insignificant 影响,因为 DBMS 倾向于 analyze/optimize 查询并将其缓存以供下一次使用采用。考虑一下:

### parameterized queries
DBI::dbGetQuery("select ... where OrderDate >= ?", params=list("2020-02-02"))
DBI::dbGetQuery("select ... where OrderDate >= ?", params=list("2020-02-03"))

### glue_sql
PeriodEndDate <- as.Date("2020-02-02")
qry <- glue::glue_sql("select ... where OrderDate >= {PeriodEndDate}", .con=con)
# <SQL> select ... where OrderDate >= '2020-02-02'
DBI::dbGetQuery(con, qry)
PeriodEndDate <- as.Date("2021-12-22")
qry <- glue::glue_sql("select ... where OrderDate >= {PeriodEndDate}", .con=con)
# <SQL> select ... where OrderDate >= '2021-12-22'
DBI::dbGetQuery(con, qry)

在参数化查询的情况下,“查询”本身永远不会改变,因此可以重用其优化查询(服务器内部)。

glue_sql 查询的情况下,查询本身会发生变化(尽管只是少数字符),因此大多数(所有?)DBMS 将 re-analyze 和 re-optimize询问。虽然他们倾向于快速完成,并且大多数分析师的查询并不复杂,但它仍然是不必要的开销,并且在您的查询 and/or 索引需要更多工作来优化的情况下错过机会 .


备注:

  1. ? 被大多数 DBMS 使用,但不是全部。其他人使用 $name</code> 等。但是,对于 <code>odbc::odbc(),它始终是 ?(无名称,无编号),而不管实际的 DBMS。

  2. 不确定你是否在别处使用这个,但是使用<<-(副<-=)会助长坏习惯and/or unreliable/unexpected 个结果。

  3. 在一个查询中多次使用同一个变量的情况并不少见。不幸的是,您将需要多次包含该变量,并且顺序很重要。例如,

    samplequery <- dbGetQuery(con, "
         SELECT * FROM [TableName]
         WHERE OrderDate <= ?
           or (SomethingElse = ? and OrderDate > ?)0
    ", params = list(PeriodEndDate, 99, PeriodEndDate))
    
  4. 如果您有 list/vector 个值并想使用 SQL 的 IN 运算符,那么您有两个选择,我的偏好是第一个(出于上述原因):

    1. 创建一串问号并粘贴到查询中。 (是的,这是 pasteing 到查询中,但我们没有处理不正确 single-quoting 或 double-quoting 的风险。因为 DBI 不支持任何其他机制,这就是我们所拥有的。)

      MyDates <- c(..., ...)
      qmarks <- paste(rep("?", length(MyDates)), collapse=",")
      samplequery <- dbGetQuery(con, sprintf("
           SELECT * FROM [TableName]
           WHERE OrderDate IN (%s)
      ", qmarks), params = as.list(MyDates))
      
    2. glue_sql支持内扩:

      MyDates <- c(..., ...)
      qry <- glue::glue_sql("
           SELECT * FROM [TableName]
           WHERE OrderDate IN ({MyDates*})", .con=con)
      DBI::dbGetQuery(con, qry)