您的第一个查询... 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 上进行了测试,结果非常相似。
result2 和result3 在所有三个 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 次而不是任何事情缓存命中/未命中。