R DBI绑定变量性能

R DBI bind variable performance

我在将绑定变量与 DBI 包一起使用时遇到性能问题。我最初的用例是使用 Postgres 数据库,但为了可重现性,下面我使用内存中的 SQLite 具有完全相同的问题 - 当我 select 来自某些 table 的数据时(在 Postgres 中,该列被索引)参数化版本在 selecting 行数上的运行时间比 SQL 多倍,ID 粘贴在 IN 语句中:

library(DBI)
library(tictoc)

sample.data <- data.frame(
  id = 1:100000,
  value = rnorm(100000)
)

sqlite <- dbConnect(RSQLite::SQLite(), ":memory:")
dbWriteTable(
  sqlite, "sample_data",
  sample.data,
  overwrite = T
)

tic("Load by bind")
ids <- 50000:50100
res <- dbSendQuery(sqlite, "SELECT * FROM sample_data WHERE id = ")
dbBind(res, list(ids))
result <- dbFetch(res)
dbClearResult(res)
toc()

# Load by bind: 0.81 sec elapsed

tic("Load by paste")
ids <- 50000:50100
result2 <- dbGetQuery(sqlite, paste0("SELECT * FROM sample_data WHERE id IN (", paste(ids, collapse = ","), ")"))
toc()

# Load by paste: 0.04 sec elapsed

似乎我应该有一些明显的错误,因为准备好的查询应该更快(我确实在同一个 Postgres 示例中用 Python/SQLAlchemy 看到了它)。

你的第一个查询 ... id = 被执行了 101 次;您的第二个查询 ... id in (..) 执行一次。如果您在 DBMS 端进行审核(此处未进行演示),那么您会看到 101 个单独的查询。

前面,一个常见的错误是简化修改语句以使用 IN (?) 子句,

dbGetQuery(pgcon, "SELECT * FROM sample_data WHERE id in (?)", params = list(ids))

但这也执行了 101 次查询,感觉与 result1.

相同的性能问题

要使用更高效的 IN (..) 子句进行参数绑定,您需要提供那么多问号(或美元数字)。

bench::mark(
  result1 = dbGetQuery(sqlite, "SELECT * FROM sample_data WHERE id = ", params = list(ids)),
  result2 = dbGetQuery(sqlite, paste0("SELECT * FROM sample_data WHERE id IN (", idcommas, ")")),
  result3 = dbGetQuery(sqlite, paste0("SELECT * FROM sample_data WHERE id IN (", qmarks, ")"),
                       params = as.list(ids)),
  min_iterations = 50
)
# # A tibble: 3 x 13
#   expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result             memory                  time          gc               
#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>             <list>                  <list>        <list>           
# 1 result1    280.97ms  347.4ms      2.86    20.6KB        0    50     0      17.5s <df[,2] [101 x 2]> <Rprofmem[,3] [14 x 3]> <bch:tm [50]> <tibble [50 x 3]>
# 2 result2      7.31ms   8.21ms    115.      15.6KB        0    58     0    502.2ms <df[,2] [101 x 2]> <Rprofmem[,3] [12 x 3]> <bch:tm [58]> <tibble [58 x 3]>
# 3 result3      7.57ms   8.93ms    113.      28.4KB        0    57     0      506ms <df[,2] [101 x 2]> <Rprofmem[,3] [28 x 3]> <bch:tm [57]> <tibble [57 x 3]>

如果你很好奇,它在 postgres 实例上执行相同(显着 更快)(尽管我将你的 </code> 更改为 <code>?: sqlite 两者都接受,[​​=74=] 只支持 qmarks):

pgcon <- dbConnect(odbc::odbc(), ...) # local docker postgres instance
bench::mark(...)
# # A tibble: 3 x 13
#   expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result             memory                  time          gc               
#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>             <list>                  <list>        <list>           
# 1 result1     967.4ms    1.05s     0.933    20.6KB        0    50     0     53.57s <df[,2] [101 x 2]> <Rprofmem[,3] [14 x 3]> <bch:tm [50]> <tibble [50 x 3]>
# 2 result2        57ms   67.7ms    14.4      18.1KB        0    50     0      3.47s <df[,2] [101 x 2]> <Rprofmem[,3] [13 x 3]> <bch:tm [50]> <tibble [50 x 3]>
# 3 result3      56.9ms  65.17ms    14.3      21.4KB        0    50     0       3.5s <df[,2] [101 x 2]> <Rprofmem[,3] [15 x 3]> <bch:tm [50]> <tibble [50 x 3]>

我也在 odbc/sql-server 上进行了测试,结果非常相似。

result2result3 在所有三个 DBMS 上通常都非常接近,事实上在不同的采样上前者比后者快,所以我将它们的性能比较称为洗牌.那么,使用绑定的动机是什么?在许多情况下,这主要是学术讨论:大多数时候,您不使用它并没有做错什么(而是使用您的 paste(ids, collapse=",") 方法)。

但是:

  • 不慎“sql注入”。从技术上讲,SQL 注入必须是恶意的才能被标记为这样,但我在 SQL 查询中非正式地归因于数据嵌入引号的“糟糕”时刻,并将其粘贴到静态查询字符串中,我打破了报价。对我来说幸运的是,它所做的只是破坏了查询的解析,我没有 deleted this year's student records.

    一个常见的错误是尝试使用 sQuote 来转义嵌入的引号。长话短说:不,SQL 的做法不同。许多 SQL 用户不知道要转义嵌入的单引号,必须将其加倍:

    sQuote("he's Irish")
    # [1] "'he's Irish'"                      # WRONG
    
    DBI::dbQuoteString(sqlite, "he's Irish")
    # <SQL> 'he''s Irish'                     # RIGHT for both sqlite and pgcon
    
  • 查询优化。大多数(全部?我不确定)DBMS 会进行某种形式的查询优化,试图利用索引 and/or 类似的措施。为了擅长它,这种优化是针对查询进行一次,然后进行缓存。但是,即使您更改了查询的一个字母,您也有缓存未命中的风险(我并不是说“总是”,因为我没有审核缓存代码……但前提是明确的,我认为)。这意味着将查询从 select * from mytable where a=1 更改为 ... a=2 确实 不会 获得缓存命中,并且(再次)对其进行了优化。

    将其与 select * from mytable where a=? 与参数绑定进行对比,您将受益于缓存。

    请注意,如果您的 ids 列表长度发生变化,则查询可能会重新优化(从 id in (?,?) 更改为 id in (?,?,?));如果那真的是缓存未命中,我不知道,同样没有审核 DBMS 代码。

顺便说一句:您提到的 “准备好的语句” 与此查询优化非常一致,但您遇到的性能损失更多是关于 运行相同的查询比与缓存有关的任何查询都多 101 次 hits/misses.