【问题标题】:R DBI 绑定变量性能
【发布时间】:2022-01-23 16:46:29
【问题描述】:

在将绑定变量与 DBI 包一起使用时,我遇到了性能问题。我最初的用例是使用 Postgres 数据库,但为了可重复性,下面我使用内存中的 SQLite,它具有完全相同的问题 - 当我通过 id 从某个表中选择数据时(在 Postgres 中,列被索引)参数化版本运行多次选择行数比在IN 语句中粘贴 ID 的 SQL 更长:

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 = $1")
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 看到了它)。

【问题讨论】:

    标签: r postgresql sqlite dbi


    【解决方案1】:

    您的第一个查询... id = $1 执行了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 = $1", 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 实例上执行相同(明显更快)(尽管我将您的 $1 更改为 ?:sqlite 接受两者,odbc/postgres 仅支持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 查询中非正式地将“oops”时刻归因于数据嵌入引号的情况,并且通过将其粘贴到静态查询字符串中,我打破了引用。对我来说幸运的是,它所做的只是打破了查询的解析,我没有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 进行某种形式的查询优化,试图利用索引和/或类似措施。为了擅长它,这种优化是为查询完成一次,然后缓存。但是,即使您更改查询的一个字母,也会冒缓存未命中的风险(我不是说“总是”,因为我没有审核缓存代码……但前提是明确的,我认为)。这意味着将查询从 select * from mytable where a=1 更改为 ... a=2 确实不会获得缓存命中,并且它被优化(再次)。

      与带有参数绑定的select * from mytable where a=? 相比,您将从缓存中受益。

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

    顺便说一句:您提到 “prepared statement” 与此查询优化非常一致,但是您遇到的性能损失更多是因为运行相同的查询 101 次而不是任何事情缓存命中/未命中。

    【讨论】:

    • 感谢您的全面回答!在参数中粘贴问号似乎仍然不是一个好的选择。可能您知道如何为需要像这样的 Postgres 数组的查询绑定参数:SELECT * FROM sample_data WHERE id = ANY(?)
    • 我为此使用了 odbc 驱动程序,粘贴问号是 ODBC 连接的唯一选择。但是,前提是相同的,如果您更喜欢使用美元数字:paste(paste0("$", seq_along(ids)), collapse=",") 应该可以工作。
    • 虽然 ANY 被不同的 DBMS 支持,但并不完全相同:postgres 支持 "select * from sample_data where id = any(array[1,2,3])",但非 postgres 不支持这种类型的语法; SQL Server 的ANY 有点不同,根本不支持这种机制。由于您开始使用 postgres 和 sqlite 作为演示数据库,因此您对 ANY(.) 的建议超出了范围。 (此外,? 是 ODBC 的标准,正如我所说,因此称其为“不是一个好的选择”似乎为时过早。我更喜欢 ODBC 支持命名绑定,但事实并非如此,我们坚持使用 ? .)
    • 我正在寻找 Postgres 的 ANY(.),因为这将允许对任意数量的参数重复使用单个准备好的语句,而无需使用适当数量的 ? 重新生成查询,无论如何,我知道这已经是另一回事了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-06-14
    • 2023-03-04
    • 2018-08-15
    • 2013-08-23
    • 1970-01-01
    • 1970-01-01
    • 2014-02-19
    相关资源
    最近更新 更多