我有一个Excel工作簿,它向数据库发出三个查询以填充隐藏工作表上的三个表,然后运行三个“刷新”脚本将这些数据拉到三个可见的演示文稿表(每个查询一个)。同步运行这个很慢:刷新的总时间是三个查询中每个查询的时间总和加上每个“刷新”脚本运行的时间总和。
我知道VBA不是多线程的,但我认为可以通过异步触发查询来加快速度(从而允许在执行时进行一些清理工作) ),然后在数据返回时为每个工作表执行填充/刷新工作。
我重写了我的脚本如下(注意我必须删除连接字符串,查询字符串等并使变量通用):
Private WithEvents cnA As ADODB.Connection
Private WithEvents cnB As ADODB.Connection
Private WithEvents cnC As ADODB.Connection
Private Sub StartingPoint()
'For brevity, only listing set-up of cnA here. You can assume identical
'set-up for cnB and cnC
Set cnA = New ADODB.Connection
Dim connectionString As String: connectionString = "<my conn string>"
cnA.connectionString = connectionString
Debug.Print "Firing cnA query: " & Now
cnA.Open
cnA.Execute "<select query>", adAsyncExecute 'takes roughly 5 seconds to execute
Debug.Print "Firing cnB query: " & Now
cnB.Open
cnB.Execute "<select query>", adAsyncExecute 'takes roughly 10 seconds to execute
Debug.Print "Firing cnC query: " & Now
cnC.Open
cnC.Execute "<select query>", adAsyncExecute 'takes roughly 20 seconds to execute
Debug.Print "Clearing workbook tables: " & Now
ClearAllTables
TablesCleared = True
Debug.Print "Tables cleared: " & Now
End Sub
Private Sub cnA_ExecuteComplete(ByVal RecordsAffected As Long, ...)
Debug.Print "cnA records received: " & Now
'Code to handle the recordset, refresh the relevant presentation sheet here,
'takes roughly < 1 seconds to complete
Debug.Print "Sheet1 tables received: " & Now
End Sub
Private Sub cnB_ExecuteComplete(ByVal RecordsAffected As Long, ...)
Debug.Print "cnB records received: " & Now
'Code to handle the recordset, refresh the relevant presentation sheet here,
'takes roughly 2-3 seconds to complete
Debug.Print "Sheet2 tables received: " & Now
End Sub
Private Sub cnC_ExecuteComplete(ByVal RecordsAffected As Long, ...)
Debug.Print "cnC records received: " & Now
'Code to handle the recordset, refresh the relevant presentation sheet here,
'takes roughly 5-7 seconds to complete
Debug.Print "Sheet3 tables received: " & Now
End Sub
典型的预期调试器输出:
Firing cnA query: 21/02/2014 10:34:22
Firing cnB query: 21/02/2014 10:34:22
Firing cnC query: 21/02/2014 10:34:22
Clearing tables: 21/02/2014 10:34:22
Tables cleared: 21/02/2014 10:34:22
cnB records received: 21/02/2014 10:34:26
Sheet2 tables refreshed: 21/02/2014 10:34:27
cnA records received: 21/02/2014 10:34:28
Sheet1 tables refreshed: 21/02/2014 10:34:28
cnC records received: 21/02/2014 10:34:34
Sheet3 tables refreshed: 21/02/2014 10:34:40
当然,三个查询可以以不同的顺序返回,具体取决于哪个完成,所以有时典型的输出的排序方式不同 - 这是预期的。
但有时候,cnX_ExecuteComplete
回调中的一个或两个根本不会发射。经过一段时间的调试,我很确定这样做的原因是,如果一个记录集在其中一个回调当前正在执行时返回,则不会发生调用。例如:
cnA_ExecuteComplete
触发cnA_ExecuteComplete
仍在投放,因此cnB_ExecuteComplete
永远不会触发cnA_ExecuteComplete
在时间8完成cnC_ExecuteComplete
触发我的理论是否正确,这是问题所在?如果是这样,是否可以解决这个问题,或者在当前代码执行之前接受“等待”调用而不是仅仅消失?
一种解决方案是在cnX_ExecuteComplete
回调期间非常快速地执行某些操作(例如,单行Set sheet1RS = pRecordset
并检查是否在完成同步运行刷新脚本之前完成了所有操作因此,它们重叠的几率几乎为零,但想先知道是否有更好的解决方案。
答案 0 :(得分:9)
我想我无法解释为什么有些'刷新脚本'并不总是会触发。这是一种奇怪的行为,有时它们会运行,有时却不会运行。我无法真正看到您的整个脚本,但我可以向您展示我是如何采用您的代码并且每次都使用它。
注意:您的问题与ExecuteComplete ADODB Connection event not fired with adAsyncExecute parameter
有某种关联我在SQL服务器上添加了3个存储过程; sp_WaitFor5
,sp_WaitFor10
,sp_WaitFor20
来模拟查询执行时间的延迟。
就像
一样简单CREATE PROCEDURE sp_WaitFor5
AS
WAITFOR DELAY '00:00:05'
所有3个延迟。
然后在我的 Module1
中添加了一个非常简单的代码来调用自定义类
Option Explicit
Private clsTest As TestEvents
Sub Main()
Cells.ClearContents
Set clsTest = New TestEvents
Call clsTest.StartingPoint
End Sub
然后我将类模块重命名为 TestEvents
并添加了稍微修改过的代码版本
Option Explicit
Private WithEvents cnA As ADODB.Connection
Private WithEvents cnB As ADODB.Connection
Private WithEvents cnC As ADODB.Connection
Private i as Long
Public Sub StartingPoint()
Dim connectionString As String: connectionString = "Driver={SQL Server};Server=MYSERVER\INST; UID=username; PWD=password!"
Debug.Print "Firing cnA query(10 sec): " & Now
Set cnA = New ADODB.Connection
cnA.connectionString = connectionString
cnA.Open
cnA.Execute "sp_WaitFor10", adExecuteNoRecords, adAsyncExecute
Debug.Print "Firing cnB query(5 sec): " & Now
Set cnB = New ADODB.Connection
cnB.connectionString = connectionString
cnB.Open
cnB.Execute "sp_WaitFor5", adExecuteNoRecords, adAsyncExecute
Debug.Print "Firing cnC query(20 sec): " & Now
Set cnC = New ADODB.Connection
cnC.connectionString = connectionString
cnC.Open
cnC.Execute "sp_WaitFor20", adExecuteNoRecords, adAsyncExecute
End Sub
Private Sub cnA_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
Debug.Print vbTab & "cnA_executeComplete START", Now
For i = 1 To 55
Range("A" & i) = Rnd(1)
Next i
Debug.Print vbTab & "cnA_executeComplete ENDED", Now
End Sub
Private Sub cnB_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
Debug.Print vbTab & "cnB_executeComplete START", Now
For i = 1 To 1000000
Range("B" & i) = Rnd(1)
Next i
Debug.Print vbTab & "cnB_executeComplete ENDED", Now
End Sub
Private Sub cnC_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
Debug.Print vbTab & "cnC_executeComplete START", Now
For i = 1 To 55
Range("C" & i) = Rnd(1)
Next i
Debug.Print vbTab & "cnC_executeComplete ENDED", Now
End Sub
除了Execute
的{{3}}和一些填充活动表只需花费时间的代码之外,我的确没有太多变化。
现在,我可以运行不同的变体/配置。我可以旋转连接对象的执行时间。我可以cnA
5秒,cnB
10秒,cnC
20秒。我可以交换/调整每个_ExecuteComplete
事件的执行时间。
从我自己的测试中我可以向你保证,所有3个始终执行。
以下是基于与您类似的配置的一些日志
Firing cnA query(10 sec): 24/02/2014 12:59:46
Firing cnB query(5 sec): 24/02/2014 12:59:46
Firing cnC query(20 sec): 24/02/2014 12:59:46
cnB_executeComplete START 24/02/2014 12:59:51
cnB_executeComplete ENDED 24/02/2014 13:00:21
cnA_executeComplete START 24/02/2014 13:00:21
cnA_executeComplete ENDED 24/02/2014 13:00:21
cnC_executeComplete START 24/02/2014 13:00:22
cnC_executeComplete ENDED 24/02/2014 13:00:22
在上面的示例中,您可以看到,所有3个查询都是异步触发的。
cnA
在5秒后返回句柄,这使得cnB
第一个让事件('刷新脚本')在层次结构中运行cnC
花费的时间最长。
由于cnB
首先返回,它会触发cnB_ExecuteComplete
事件过程。 cnB_ExecuteComplete
本身设置需要花费一些时间执行(迭代100万次并用随机数填充B列。注意:cnA填充A列,cnB col B,cnC col C) 。查看上面的日志需要30秒才能运行。
当cnB_ExecuteComplete
正在执行其工作/占用资源(并且您知道VBA是单线程)时,cnA_ExecuteComplete
事件将添加到TODO的队列中流程。所以,你可以把它想象成一个队列。当事情正在被照顾下一件事时,必须等到最后。
如果我更改配置; cnA
5秒,cnB
10秒,cnC
20秒,让每个'刷新脚本'迭代100万次
Firing cnA query(5 sec): 24/02/2014 13:17:10
Firing cnB query(10 sec): 24/02/2014 13:17:10
Firing cnC query(20 sec): 24/02/2014 13:17:10
one million iterations each
cnA_executeComplete START 24/02/2014 13:17:15
cnA_executeComplete ENDED 24/02/2014 13:17:45
cnB_executeComplete START 24/02/2014 13:17:45
cnB_executeComplete ENDED 24/02/2014 13:18:14
cnC_executeComplete START 24/02/2014 13:18:14
cnC_executeComplete ENDED 24/02/2014 13:18:44
从第一个例子中明确证明了我的观点。
此外,尝试cnA
5秒,cnB
5秒,cnC
5秒
Firing cnA query(5 sec): 24/02/2014 13:20:56
Firing cnB query(5 sec): 24/02/2014 13:20:56
Firing cnC query(5 sec): 24/02/2014 13:20:56
one million iterations each
cnB_executeComplete START 24/02/2014 13:21:01
cnB_executeComplete ENDED 24/02/2014 13:21:31
cnA_executeComplete START 24/02/2014 13:21:31
cnA_executeComplete ENDED 24/02/2014 13:22:01
cnC_executeComplete START 24/02/2014 13:22:01
cnC_executeComplete ENDED 24/02/2014 13:22:31
其中也完成/执行所有3。
就像我说的,我看不到你的整个代码,也许你的代码中某处有一个未处理的错误,也许有些误导你认为一个_ExecuteComplete
根本没有执行。尝试更改您的代码以反映我给您的代码并自行运行一些文本。我期待着您的反馈。
答案 1 :(得分:1)
我也不确定为什么事件并不总是被你解雇 对我来说,测试始终有效(测试了10万行和14列),但我不确定您的数据库的大小和您正在执行的查询的复杂性。
虽然我有一个评论。
ExecuteComplete
和FetchComplete
事件之间存在重要差异。
命令执行完毕后会发出ExecuteComplete
(在您的示例中,命令对象由ADO内部创建)。这并不一定意味着在此回调触发时已获取所有记录。
因此,如果您需要使用返回的记录集,则应该监听fetchComplete
回调,该回调仅在完全提取记录集时触发。
答案 2 :(得分:0)
我可以给你一个答案,它可以帮助你一些时间,但不是所有时间。
有时,您的Recordset.Open或Command.Execute会忽略AdAsynchFetch
参数。
也就是说:当您请求时,问题立即显现,并且当ADODB使用已填充的记录集回调时,应用程序处于无响应状态时不会出现问题。
幸运的是,这是你可以在代码中捕获的东西;当忽略AdFetchAsynch时会发生三件事:
ExecuteComplete
事件。 你可以看到我的目标......
如果您的记录集请求代码在退出之前检测到打开的记录集,请将打开的记录集直接传递到现有的_FetchComplete
事件过程中:
Set m_rst = New ADODB.Recordset ' declared at module level With Events
With m_rst
Set .ActiveConnection = ThisWorkbook.MyDBConnection
.CursorType = adOpenForwardOnly
Err.Clear
.Open SQL, , , , adCmdText + adAsyncFetch
End With
If m_rst.State = adStateOpen Then
' This block will only run if the adAsyncFetch flag is ignored
If m_rst.EOF And m_rst.BOF Then
MsgPopup "No matching data for " & DATASET_NAME, vbExclamation + vbOKOnly, "Empty data set", 90
ElseIf m_rst.EOF Then
m_rst.MoveFirst
m_rst_FetchComplete Nothing, GetStatus(m_rst), m_rst
Else
m_rst_FetchComplete Nothing, GetStatus(m_rst), m_rst
End If
Set m_rst = Nothing
ElseIf m_rst.ActiveConnection.Errors.Count > 0 Then
m_rst_FetchComplete m_rst.ActiveConnection.Errors(0), adStatusErrorsOccurred, m_rst
Set m_rst = Nothing
ElseIf Err.Number <> 0 Then
MsgPopup "Microsoft Excel returned error &H" & Hex(Err.Number) & ": " & Err.Description, vbCritical + vbOKOnly, "Error requesting " & DATASET_NAME, 60
Set m_rst = Nothing
ElseIf m_rst.State < adStateOpen Then
MsgPopup "Microsoft Excel was unable to request data for " & DATASET_NAME & ": no error information is available", vbCritical + vbOKOnly, "Error requesting " & DATASET_NAME, 60
Set m_rst = Nothing
Else
' Fetch progess is not available with the OLEDB driver I am using
' m_rst_FetchProgress 0, 100, GetStatus(m_rst), m_rst
End If
如果永远不会提出_FetchComplete
事件,显然这将毫无用处:&#39; open&#39;异步运行并且该方法以状态adStateConnecting或adStateFetching中的记录集退出,并且您完全依赖于m_rst_FetchComplete
事件过程。
但这在某些时候解决了这个问题。
下一步:当您在以太网中输出记录集请求时,您需要检查Application.EnableEvents
是否永远不会设置为false。我猜你已经想到了这一点,但这是我能想到的唯一一件事。
此外:
对于不熟悉ADODB编码的读者的提示:请考虑使用adCmdStoredProc
并按名称调用已保存的查询或记录集返回功能,而不是使用&#39; SELECT * FROM&#39;和adCmdText
。
这里的答案很晚,但其他人也会遇到同样的问题。