改进 Excel VBA 中的 SQL 查询
Posted
技术标签:
【中文标题】改进 Excel VBA 中的 SQL 查询【英文标题】:Improve SQL query in Excel VBA 【发布时间】:2019-07-24 15:02:49 【问题描述】:我在 Excel 工作簿中有 3 个可以使用 SQL 访问的表。
Inscriptions 表包含 AGENT_ID
和 MLS_ID
,PHOTOS
表包含 MLS_ID
的最近提要中的所有照片,PHOTOS_CURRENT
包含当前系统中的所有照片MLS_ID
.
目标是查找新提要中是否有当前不在系统中的照片。
我尝试使用NOT EXISTS
和NOT IN
方法进行查询。两者都需要很长时间才能运行(有时每个 AGENT_ID
需要 2 分钟)。
NOT EXISTS
方法:
sqlQuery = "SELECT DISTINCT INSCR.MLS_ID FROM [INSCRIPTIONS_CURRENT$] INSCR, [PHOTOS$] P1 " & _
"WHERE INSCR.AGENT_ID = " & inpAgentId & _
" AND INSCR.MLS_ID = P1.MLS_ID AND NOT exists (select 1 from [PHOTOS_CURRENT$] PC1 where PC1.MLS_ID = P1.MLS_ID and PC1.PHOTO_ID = P1.PHOTO_ID)"
NOT IN
方法:
sqlQuery = "SELECT DISTINCT INSCR.MLS_ID FROM [INSCRIPTIONS_CURRENT$] INSCR, [PHOTOS$] P1 " & _
"WHERE INSCR.AGENT_ID = " & inpAgentId & _
" AND INSCR.MLS_ID = P1.MLS_ID AND INSCR.MLS_ID NOT IN (select MLS_ID from [PHOTOS_CURRENT$] PC1 where PC1.MLS_ID = P1.MLS_ID and PC1.PHOTO_ID = P1.PHOTO_ID)"
数据库连接如下:
Sub Connect()
Set objConnection = CreateObject("ADODB.Connection")
objConnection.CommandTimeout = 120
End Sub
查询发送到程序处理如下:
Function select_query(sqlQuery As String) As ADODB.Recordset
Dim objRecordset As ADODB.Recordset
Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H1
Set objRecordset = CreateObject("ADODB.Recordset")
objConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source=" & ThisWorkbook.FullName & _
";Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"";"
objRecordset.Open sqlQuery, objConnection, adOpenStatic, adLockOptimistic,
adCmdText
Set select_query = objRecordset
End Function
有什么提高性能的建议吗?
【问题讨论】:
桌子有多大? @TimWilliams PHOTOS 和 PHOTOS_CURRENT 中约有 20,000 条记录,INSCRIPTIONS 中约有 2,000 条记录 请提供更完整的代码块而不是行 sn-ps 以便我们可以看到整个过程。否则,请记住 excel is not a database,因此请使用与其兄弟ms access
类似的实际名称,它可以索引字段以加快表扫描!
@Parfait 在我已经提供的内容中添加任何其他内容没有多大意义。如果查询找到任何记录,则将它们插入输出数组并发送回调用例程。我知道 Excel 不是一个真正的数据库,我在公司强加给我的限制条件下工作。这个 Excel 有大约 20 个工作表,用作数据库,取它或留下它。我向公司提出了改进建议,但当他们做出正确的决定时,我必须让这段代码工作。
如果您在循环中运行它,您似乎可以从 NOT EXISTS
查询中创建一个表并加入它,而不是为每个代理重复该查询。
【参考方案1】:
考虑以下可能有帮助的提示:
Explicit JOIN:现在您正在运行过时的隐式连接,它与 WHERE
子句中的 ID 匹配,而不是显式 JOIN
子句的当前标准。在大多数数据库引擎中,这不应该改变性能,但轶事证据表明它可以:
SELECT DISTINCT INSCR.MLS_ID
FROM [INSCRIPTIONS_CURRENT$] INSCR
INNER JOIN [PHOTOS$] P1 ON INSCR.MLS_ID = P1.MLS_ID
WHERE INSCR.AGENT_ID = " & inpAgentId & _
AND NOT EXISTS (select 1 from [PHOTOS_CURRENT$] PC1
where PC1.MLS_ID = P1.MLS_ID and PC1.PHOTO_ID = P1.PHOTO_ID)
GROUP BY vs DISTINCT:这是 SQL 中的常规辩论,不同的数据库引擎以不同的方式处理非重复查询。从理论上讲,性能应该没有差异,但轶事证据表明并非如此。因此,考虑一个等效的GROUP BY
版本:
SELECT INSCR.MLS_ID
FROM [INSCRIPTIONS_CURRENT$] INSCR
INNER JOIN [PHOTOS$] P1 ON INSCR.MLS_ID = P1.MLS_ID
WHERE INSCR.AGENT_ID = " & inpAgentId & _
AND NOT EXISTS (select 1 from [PHOTOS_CURRENT$] PC1
where PC1.MLS_ID = P1.MLS_ID and PC1.PHOTO_ID = P1.PHOTO_ID)
GROUP BY INSCR.MLS_ID
DAO 连接:由于查询工作簿使用 JET/ACE SQL 引擎,因此将 DAO 视为可以利用该引擎的许多优点的特定接口,而不是 ADO 跨任何数据源的更通用的接口( Oracle、SQL Server、Postgres 等)。
' ADD REFERENCE: Microsoft Office #.# Access Database Engine Object Library
Dim conn As New DAO.DBEngine, db As DAO.Database, qdef As DAO.QueryDef, rst As DAO.Recordset
Set db = conn.OpenDatabase("C:\Path\To\Workbook.xls", False, True, "Excel 8.0;HDR=Yes;")
Set rst = db.OpenRecordset(sqlQuery)
...
rst.Close: db.Close
Set rst = Nothing: Set db = Nothing: Set conn = Nothing
OLEDB (ACE) 连接:考虑更新的 OLEDB 提供程序,它仍然可以与任何版本的 Excel(.xls 或 .xlsx、.xlsb、.xlsm)一起使用。使用此PowerShell script 检查可用的提供商。
objConnection.Open "Provider=Microsoft.ACE.OLEDB.12.0;" ...
objConnection.Open "Provider=Microsoft.ACE.OLEDB.16.0;" ...
ODBC 连接:在传闻证据与理论不同的情况下,连接接口可能会带来不同的查询执行性能。因此,请考虑更换 OLEDB 提供程序以进行 ODBC 驱动程序连接:
' DRIVER VERSION
objConnection.Open "DRIVER=Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb);" _
& "DBQ=C:\Path\To\Excel.xls;"
' DSN VERSION
objConnection.Open "DSN=Excel Files;DBQ=C:\Path\To\Excel.xls;"
光标/锁定类型:试用cursor types,因为性能可能会有所不同,例如adOpenForwardOnly
vs adOpenStatic
,甚至LockType 和adLockOptimistic
vs adLockReadOnly
.
【讨论】:
谢谢@Parfait。如果我有能力改变整个系统,你的建议会有所帮助。不幸的是,正如我所说,我必须使用现有的 Connect-Select-Disconnect 方法,此 Excel 数据库中的数百个查询都使用该方法。完整的回归测试是毫无疑问的。我宁愿等待他们批准迁移到真正的数据库并让它在那里工作。 我不理解您的评论。所有这些解决方案都可以在 Excel 中处理。【参考方案2】:感谢@TimWilliams,您的评论对解决这个问题很有帮助。 我最终做的是编写一个单独的例程,该例程在提要加载期间创建一个包含所有更改的照片的表格,如下所示:
sqlQuery = "INSERT INTO [PHOTO_UPDATES$] SELECT P1.* " & _
"FROM [PHOTOS$] P1 LEFT JOIN [PHOTOS_CURRENT$] PC1 " & _
"ON P1.MLS_ID = PC1.MLS_ID AND P1.PHOTO_ID = PC1.PHOTO_ID WHERE PC1.PHOTO_ID is NULL"
然后,在为每个代理创建工作列表时,将完成以下操作:
sqlQuery = "SELECT DISTINCT INSCR.MLS_ID " & _
"FROM [PHOTO_UPDATES$] PU1 , [INSCRIPTIONS_CURRENT$] INSCR " & _
"WHERE INSCR.AGENT_ID = " & inpAgentId & " " & _
"AND PU1.MLS_ID = INSCR.MLS_ID "
这两个例程的运行时间都不到 1 秒。
【讨论】:
以上是关于改进 Excel VBA 中的 SQL 查询的主要内容,如果未能解决你的问题,请参考以下文章
EXCEL VBA combobox 模糊查询触发后 退格键功能改变
EXCEL VBA combobox 模糊查询触发后 退格键功能改变
更新表时如何改进 Spark 中的 SQL 查询? (子查询中的'NOT IN')