pauloaguia
asked on
QueryTable with BackgroundQuery in Excel
Here's the story:
I have an Excel document that generates reports from a DB's contents. I have a form that collects the report parameters and that on the click of a button generates the report.
However the DB contents are huge and I also have a Cancel button to stop the report if at some point it's obvious it's going to take too long to generate...
I coded the Cancel button just fine. But if I hit it during the execution of one of the queries it just wouldn't Cancel until the query finished (which could be up to 5 min).
"Easy" - I thought - " just turn it into a background query so you can cancel it during execution". However the Refreshing property of the QueryTable doesn't seem to change until I hit Ctrl+Break to Debug the code and see why the query never finishes execution.
You can check this for yourselves: Just paste the following code on the ThisWorkbook module of a new workbook and change the query to one that will work on a DB of yours and hit F5 (since the ODBC connect string is incomplete it will prompt you to choose the ODBC connection)
---8<---8<---8<---
Sub fillin()
Dim qt As QueryTable
Set qt = Application.ActiveSheet.Qu eryTables. Add("ODBC; ", Range("A1"), "SELECT * FROM tabela")
qt.BackgroundQuery = True
qt.FieldNames = True
qt.Refresh
Do While qt.Refreshing
DoEvents
Loop
End Sub
---8<---8<---8<---
Even for a query that should last a second, it will be stuck... Hit Ctrl+Break to debug and "magically" the refreshing property goes to False and you'll get out of the loop...
By the way: I'm using the DoEvents method inside the loop since I need to capture the Click event on the Cancel Button. I've also tried adding a "Wait 1 sec" inside the loop but the result is the same.
What am I doing wrong here? And why is the Refreshing property behaving like that? What's the way to accomplish this task? I've looked everywhere but I've found nothing on the subject...
I have an Excel document that generates reports from a DB's contents. I have a form that collects the report parameters and that on the click of a button generates the report.
However the DB contents are huge and I also have a Cancel button to stop the report if at some point it's obvious it's going to take too long to generate...
I coded the Cancel button just fine. But if I hit it during the execution of one of the queries it just wouldn't Cancel until the query finished (which could be up to 5 min).
"Easy" - I thought - " just turn it into a background query so you can cancel it during execution". However the Refreshing property of the QueryTable doesn't seem to change until I hit Ctrl+Break to Debug the code and see why the query never finishes execution.
You can check this for yourselves: Just paste the following code on the ThisWorkbook module of a new workbook and change the query to one that will work on a DB of yours and hit F5 (since the ODBC connect string is incomplete it will prompt you to choose the ODBC connection)
---8<---8<---8<---
Sub fillin()
Dim qt As QueryTable
Set qt = Application.ActiveSheet.Qu
qt.BackgroundQuery = True
qt.FieldNames = True
qt.Refresh
Do While qt.Refreshing
DoEvents
Loop
End Sub
---8<---8<---8<---
Even for a query that should last a second, it will be stuck... Hit Ctrl+Break to debug and "magically" the refreshing property goes to False and you'll get out of the loop...
By the way: I'm using the DoEvents method inside the loop since I need to capture the Click event on the Cancel Button. I've also tried adding a "Wait 1 sec" inside the loop but the result is the same.
What am I doing wrong here? And why is the Refreshing property behaving like that? What's the way to accomplish this task? I've looked everywhere but I've found nothing on the subject...
ASKER
Zmey2,
I have a few problems with your function:
* This doesn't seem to perform an asynchronous connection. That's the reason I got in this in the first place. If I'm not mistaken that's just a question of setting an option when opening the recordset (I'll find that out)
* I suppose your comments are in russian because here they appear all messed up (nevertheless I understood it fine without them)
* I will loose some of the properties of the querytable like filtering or sorting on different fields on the fly. Which is one of the reasons I'm using it as well.
* You have optional parameters (Like WS) that you set to nothing as default but then just use them as if they were set...
I will however try to fit this into my code and if I have any success I'll let you know.
For everybody else, please, feel free to post some different solution.
I have a few problems with your function:
* This doesn't seem to perform an asynchronous connection. That's the reason I got in this in the first place. If I'm not mistaken that's just a question of setting an option when opening the recordset (I'll find that out)
* I suppose your comments are in russian because here they appear all messed up (nevertheless I understood it fine without them)
* I will loose some of the properties of the querytable like filtering or sorting on different fields on the fly. Which is one of the reasons I'm using it as well.
* You have optional parameters (Like WS) that you set to nothing as default but then just use them as if they were set...
I will however try to fit this into my code and if I have any success I'll let you know.
For everybody else, please, feel free to post some different solution.
Well, i'll explain a little bit:
Comments in Russian, sorry for that, but i thought it's quite easy to understand.
You can sort and filter your table in sql string using WHERE and ORDER BY clause.
Well, WS is a worksheet you should create before put data in it.
I recommend this function just cause it's the fastest way to put data from access to excel and you can test this.
Best regards,
Zmey2.
Comments in Russian, sorry for that, but i thought it's quite easy to understand.
You can sort and filter your table in sql string using WHERE and ORDER BY clause.
Well, WS is a worksheet you should create before put data in it.
I recommend this function just cause it's the fastest way to put data from access to excel and you can test this.
Best regards,
Zmey2.
Well, i'll explain a little bit:
Comments in Russian, sorry for that, but i thought it's quite easy to understand.
You can sort and filter your table in sql string using WHERE and ORDER BY clause.
Well, WS is a worksheet you should create before put data in it.
I recommend this function just cause it's the fastest way to put data from access to excel and you can test this.
Best regards,
Zmey2.
Comments in Russian, sorry for that, but i thought it's quite easy to understand.
You can sort and filter your table in sql string using WHERE and ORDER BY clause.
Well, WS is a worksheet you should create before put data in it.
I recommend this function just cause it's the fastest way to put data from access to excel and you can test this.
Best regards,
Zmey2.
ASKER
I understood the code... I was just teasing you about the comments. :)
And I know that I can sort using ORDER BY and filter using WHERE. But that's when I run the query. QueryTables offer me the opportunity to filter and sort data on the fly only in Excel without having to requery (up to a 5 min query, don't forget) all the data again.
Nevertheless I'm trying it... I may have some comments on my results by tomorrow (or Monday, since we're approaching the weekend)
And I know that I can sort using ORDER BY and filter using WHERE. But that's when I run the query. QueryTables offer me the opportunity to filter and sort data on the fly only in Excel without having to requery (up to a 5 min query, don't forget) all the data again.
Nevertheless I'm trying it... I may have some comments on my results by tomorrow (or Monday, since we're approaching the weekend)
Wish you a good luck, pauloaguia.
ASKER
Here's an update on my progress:
I needed to tweak your function a bit. I don't know what version of ADO you are using but I'm stuck with ADO 3.51 (it's one of the requirements. I still haven't figured out why, but I need to live with it). Anyway in that version there's no Open method for the recordset so I had to create a QueryDef to open it...
The QueryTable methods I used the most during the report generation were the FetchedRowOverflow (to check if I had "run out" of worksheet to put the results in) and the ResultRange. I can mimic the latter by making the funtion return a Range (I wouldn't need it to return a worksheet anyway). This also solves a weird problem I had because querytable's resultrange allways has a minimum of 3 rows (even if empty ones).
As to the FetchedRowOverflow (which if achieved would halt the execution) I will think of something (probably return Nothing and check the return value. Still need to think on this one to allow me to find the difference between an empty query and an overfull one).
It turned out I wasn't using a querytable when I filtered results but a PivotTable based on a querytable (with a few more columns on the side).
I also needed to open the recordset as an asynchronous one. This was easy since it's one of the options of the OpenRecordset method. Then do a cycle waiting for it to stop refreshing (with a wait of 1 sec and a DoEvents in the inside). It worked fine.
Instead of passing a worksheet and coordinates, I just pass a range representing the destination's UpperLeft corner. It's what I pass to a querytable and I don't have to change that much code in the process.
You were right about this method being faster. For a relatively simple query this method took me about 15% less time to complete. I must admit I had my doubts because of that matrix transposition but numbers don't lie :)
Now I have basically two problems:
* I'm almost at the end of the project. The application involves not one but a dozen of reports, each one using two to three querytables (which are used sparsely in the code). I'll have to recode most of it (but a man's gotta do what a man's gotta do).
* Currently my Cancel button was setting a flag in the module (each module for a report) that indicated whether execution had been halted or not. However now I'm going to add this function to a common module where I keep a few shared methods. So I won't know which of the flags to check to cancel the loop if someone hits the Cancel button..
Actually, now that I think about it I think I could pass a reference to the Form that is getting the input from the user (I have one Form per report) and make this flag a property of the form... Here's an idea to look into more carefully...
Nevertheless I will have a long recoding job ahead (which should take me a couple of days to finish). I'll get on to it and when I'm done I'll post the finished version of the function for the records.
I'd also like to wait for that event to happen to close this question just in case I find some other dificulty I can't think of right now and that will make me want to stick to querytables.
So Zmey2 your well deserved points will have to wait a little bit :)
And if anybody else is still following this thread I'm still curious as to if it is possible to accomplish this task using only querytables. I would award some extra points for the person that would come up with that solution too...
I needed to tweak your function a bit. I don't know what version of ADO you are using but I'm stuck with ADO 3.51 (it's one of the requirements. I still haven't figured out why, but I need to live with it). Anyway in that version there's no Open method for the recordset so I had to create a QueryDef to open it...
The QueryTable methods I used the most during the report generation were the FetchedRowOverflow (to check if I had "run out" of worksheet to put the results in) and the ResultRange. I can mimic the latter by making the funtion return a Range (I wouldn't need it to return a worksheet anyway). This also solves a weird problem I had because querytable's resultrange allways has a minimum of 3 rows (even if empty ones).
As to the FetchedRowOverflow (which if achieved would halt the execution) I will think of something (probably return Nothing and check the return value. Still need to think on this one to allow me to find the difference between an empty query and an overfull one).
It turned out I wasn't using a querytable when I filtered results but a PivotTable based on a querytable (with a few more columns on the side).
I also needed to open the recordset as an asynchronous one. This was easy since it's one of the options of the OpenRecordset method. Then do a cycle waiting for it to stop refreshing (with a wait of 1 sec and a DoEvents in the inside). It worked fine.
Instead of passing a worksheet and coordinates, I just pass a range representing the destination's UpperLeft corner. It's what I pass to a querytable and I don't have to change that much code in the process.
You were right about this method being faster. For a relatively simple query this method took me about 15% less time to complete. I must admit I had my doubts because of that matrix transposition but numbers don't lie :)
Now I have basically two problems:
* I'm almost at the end of the project. The application involves not one but a dozen of reports, each one using two to three querytables (which are used sparsely in the code). I'll have to recode most of it (but a man's gotta do what a man's gotta do).
* Currently my Cancel button was setting a flag in the module (each module for a report) that indicated whether execution had been halted or not. However now I'm going to add this function to a common module where I keep a few shared methods. So I won't know which of the flags to check to cancel the loop if someone hits the Cancel button..
Actually, now that I think about it I think I could pass a reference to the Form that is getting the input from the user (I have one Form per report) and make this flag a property of the form... Here's an idea to look into more carefully...
Nevertheless I will have a long recoding job ahead (which should take me a couple of days to finish). I'll get on to it and when I'm done I'll post the finished version of the function for the records.
I'd also like to wait for that event to happen to close this question just in case I find some other dificulty I can't think of right now and that will make me want to stick to querytables.
So Zmey2 your well deserved points will have to wait a little bit :)
And if anybody else is still following this thread I'm still curious as to if it is possible to accomplish this task using only querytables. I would award some extra points for the person that would come up with that solution too...
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
Sorry for the late response but apparently there's something wrong with e-mail notification (which means I don't know when you'll get this either :) )
I've tryed using ADO at first. But the thing is that in ADO the termination of an assynchronous query is flaged by an event and not a property. Which would imply that it would have to be an event that triggered the exit of the loop. Not to mention the fact htat I would still need to learn I to catch that event (all exemples I see are allways in C, I haven't found a single one in VB)
With DAO (by the way I'm using DAO 3.51 and not ADO 3.51 as I stated earlier... I switched the two characters by accident) I can check on this by using the StillExecuting property. However I'm having some problems with it also since after a while (for some of those really long queries) the checking upon the flag raises an error number 63535 "ODBC - call failed" (the query is correct. It runs on the Sql Worksheet. And it executes. The thing is, when Oracle finally decides to finish the query, the StillExecuting call fails all of a sudden).
In this scenario I'm wondering if you have or can find any VB (preferably VBA) example using and checking for an asynchronous connection in ADO.
Or do you have any ideas as to what may be happening to make the ODBC call fail in DAO?
After all asynchronism is the heart of this question.
I've tryed using ADO at first. But the thing is that in ADO the termination of an assynchronous query is flaged by an event and not a property. Which would imply that it would have to be an event that triggered the exit of the loop. Not to mention the fact htat I would still need to learn I to catch that event (all exemples I see are allways in C, I haven't found a single one in VB)
With DAO (by the way I'm using DAO 3.51 and not ADO 3.51 as I stated earlier... I switched the two characters by accident) I can check on this by using the StillExecuting property. However I'm having some problems with it also since after a while (for some of those really long queries) the checking upon the flag raises an error number 63535 "ODBC - call failed" (the query is correct. It runs on the Sql Worksheet. And it executes. The thing is, when Oracle finally decides to finish the query, the StillExecuting call fails all of a sudden).
In this scenario I'm wondering if you have or can find any VB (preferably VBA) example using and checking for an asynchronous connection in ADO.
Or do you have any ideas as to what may be happening to make the ODBC call fail in DAO?
After all asynchronism is the heart of this question.
ASKER
Never mind. I found the State property (in MSDN it just says to watch the event doesn't say that you can check the state too).
Thanks for your help and Happy New Year (I'm so happy I could finish this just before the end of the year :) )
I'll post the complete function here:
'It receives a form as an argument because the form has the Canceled flag stating if the Cancel button has been pressed or not.
Public Function Query(Sql As String, ConnectionString As String, Destination As Range, Optional Headers As Boolean = True, Optional ByRef Form As Object = Nothing) As Range
Dim a As Variant
Dim rs As ADODB.Recordset
Dim conn As ADODB.Connection
Dim c() As Variant
Dim j, k As Long
Dim lngOptions As Long
Set conn = New ADODB.Connection
conn.Open ConnectionString
Select Case ConnectionString
Case ThisWorkbook.strConnection
lngOptions = adAsyncConnect 'Oracle DB. Should connect asynchronously
Case ThisWorkbook.strConnection Config
lngOptions = -1 'Access DB. Only supports synchronous connections
End Select
Set rs = conn.Execute(CommandText:= Sql, Options:=lngOptions)
Do While rs.State = adStateConnecting Or rs.State = adStateExecuting Or rs.State = adStateFetching
Application.Wait Now + TimeValue("00:00:01")
DoEvents
If Not Form Is Nothing Then
If Form.Canceled Then
rs.Cancel
conn.Close
Set conn = Nothing
Set Query = Nothing 'If canceled doesn't really matter what it returns
Exit Function
End If
End If
Loop
If rs.EOF Then
rs.Close
conn.Close
Set conn = Nothing
Set Query = Destination 'Empty recordset. Return original destination cell only
Exit Function
End If
a = rs.GetRows(65537)
If UBound(a, 2) + 1 = 65537 Then
'If it doesn't fit on a worksheet
rs.Close
conn.Close
Set conn = Nothing
Set Query = Nothing
Exit Function
End If
If Not rs.EOF Then 'should be at EOF since all records were alegedely retrieved. If not an error as ocurred
rs.Close
conn.Close
Set conn = Nothing
Set Query = Nothing
Err.Raise vbObjectError + 1, "", "Ocorreu um erro ao tentar aceder à base de dados.", "", 0
End If
ReDim c(UBound(a, 2), UBound(a, 1))
For k = 0 To UBound(a, 1)
For j = 0 To UBound(a, 2)
c(j, k) = Trim(a(k, j))
Next j
Next k
Destination.Worksheet.Rang e(Destinat ion, Destination.Offset(UBound( a, 2), UBound(a, 1))).Value = c
If Headers Then
Destination.Worksheet.Rang e(Destinat ion, Destination.Offset(0, UBound(a, 1))).Insert xlShiftDown
For j = 0 To UBound(a, 1)
Destination.Cells(0, j + 1).Value = rs.Fields(j).Name
Next j
'Return the range with Data and HEaders
Set Query = Destination.Worksheet.Rang e(Destinat ion.Offset (-1, 0), Destination.Offset(UBound( a, 2), UBound(a, 1)))
Else
'Return the range with Data
Set Query = Destination.Worksheet.Rang e(Destinat ion, Destination.Offset(UBound( a, 2), UBound(a, 1)))
End If
rs.Close
conn.Close
Set conn = Nothing
End Function
Thanks for your help and Happy New Year (I'm so happy I could finish this just before the end of the year :) )
I'll post the complete function here:
'It receives a form as an argument because the form has the Canceled flag stating if the Cancel button has been pressed or not.
Public Function Query(Sql As String, ConnectionString As String, Destination As Range, Optional Headers As Boolean = True, Optional ByRef Form As Object = Nothing) As Range
Dim a As Variant
Dim rs As ADODB.Recordset
Dim conn As ADODB.Connection
Dim c() As Variant
Dim j, k As Long
Dim lngOptions As Long
Set conn = New ADODB.Connection
conn.Open ConnectionString
Select Case ConnectionString
Case ThisWorkbook.strConnection
lngOptions = adAsyncConnect 'Oracle DB. Should connect asynchronously
Case ThisWorkbook.strConnection
lngOptions = -1 'Access DB. Only supports synchronous connections
End Select
Set rs = conn.Execute(CommandText:=
Do While rs.State = adStateConnecting Or rs.State = adStateExecuting Or rs.State = adStateFetching
Application.Wait Now + TimeValue("00:00:01")
DoEvents
If Not Form Is Nothing Then
If Form.Canceled Then
rs.Cancel
conn.Close
Set conn = Nothing
Set Query = Nothing 'If canceled doesn't really matter what it returns
Exit Function
End If
End If
Loop
If rs.EOF Then
rs.Close
conn.Close
Set conn = Nothing
Set Query = Destination 'Empty recordset. Return original destination cell only
Exit Function
End If
a = rs.GetRows(65537)
If UBound(a, 2) + 1 = 65537 Then
'If it doesn't fit on a worksheet
rs.Close
conn.Close
Set conn = Nothing
Set Query = Nothing
Exit Function
End If
If Not rs.EOF Then 'should be at EOF since all records were alegedely retrieved. If not an error as ocurred
rs.Close
conn.Close
Set conn = Nothing
Set Query = Nothing
Err.Raise vbObjectError + 1, "", "Ocorreu um erro ao tentar aceder à base de dados.", "", 0
End If
ReDim c(UBound(a, 2), UBound(a, 1))
For k = 0 To UBound(a, 1)
For j = 0 To UBound(a, 2)
c(j, k) = Trim(a(k, j))
Next j
Next k
Destination.Worksheet.Rang
If Headers Then
Destination.Worksheet.Rang
For j = 0 To UBound(a, 1)
Destination.Cells(0, j + 1).Value = rs.Fields(j).Name
Next j
'Return the range with Data and HEaders
Set Query = Destination.Worksheet.Rang
Else
'Return the range with Data
Set Query = Destination.Worksheet.Rang
End If
rs.Close
conn.Close
Set conn = Nothing
End Function
use thist function:
Public Function XLOut(sql As String, Optional WS As Worksheet = Nothing, Optional ByRef x As Long = 1, Optional ByRef y As Long = 1, Optional ByRef n As Long = 1, Optional ByRef m As Long = 1, Optional Headers As Boolean = True) As Worksheet
'Turbo Version
Dim a As Variant
Dim rs As New ADODB.Recordset
Dim con As New ADODB.Connection
Dim ors As Recordset
'Dim l, i, j As Integer
Dim c() As Variant
Dim i, j, l, k As Integer
rs.Open sql, "Driver={Microsoft Access Driver (*.mdb)};Dbq=" & CurrentDb.Name & ";", adOpenForwardOnly, adLockOptimistic
a = rs.GetRows()
ReDim c(UBound(a, 2), UBound(a, 1))
For k = 0 To UBound(a, 1) 'ñòðîêè
For j = 0 To UBound(a, 2) 'ïîëÿ
c(j, k) = a(k, j)
Next j
Next k
n = UBound(a, 2) + 1
m = UBound(a, 1) + 1
WS.Range(WS.Cells(y, x), WS.Cells(n + y - 1, m + x - 1)) = c
'çäåñü, åñëè íóæíî, ðèñóåì çàãîëîâêè
If Headers Then
WS.Range(WS.Cells(y, x), WS.Cells(n + y - 1, m + x - 1)).rows(1).Insert
For j = 0 To m - 1
WS.Cells(y, j + x).Value = rs.Fields(j).Name
Next j
End If
rs.Close
'óñå...
Exit Function
whoops:
Resume Next
End Function
Email me notmyname on mail.ru for comments.