How can I clear pivot table cache with VBA, but not destroy pivot table structure? My pivot table is connected to external data source. The SQL source determines which user should see which portion of the data. The source populates the table as the table is refreshed. I want to save the Excel file and distribute it with clean pivot table (no data inside).
As a result I want to get exactly this:
I have experimented around this code with no success. There is no such thing like PivotCaches.Clear in VBA.
Sub PT_cache_clear()
For Each pc In ActiveWorkbook.PivotCaches
pc.Clear
Next pc
End Sub
The only good solution I found is to refresh the table with a user which has access to SQL server source but is not allowed to see any single record of the data.
The idea:
ActiveSheet.PivotTables("PivotTable1").SaveData = False
seems not to lead to desired results.
The way I do it is to refresh with a query that will return the table structure but with 0 records. So if selecting from a view something like:
select top 0 *
from vw_MyPivotData
If using a stored procedure, you can send a parameter that ensures that no records will be returned such as a filter that you know doesn't exist in the data or a special parameter devised for the purpose of returning no records.
You cannot just clear the PivotCache without affecting the pivot table, they are inexorably linked. You can trick the PivotCache into loading and empty result set with the same structure/schema. Use the PivotTable.ChangeConnection function to switch the connection before closing the document.
If you have two external connections defined in your excel file. One that returns the correct data and another that returns the same structure but no rows. You can switch the connection to the no rows version and flush the cache that way. If there are any differences between the structure/schema of the connection resultset then excel will throw an error message.
Change the connection on demand, just before you distribute your file.
Sub PT_cache_clear()
'change connection'
Dim con as vartype
Set con = ActiveWorkbook.Connections("MyNoResultConnection")
Worksheets(1).PivotTables(1).ChangeConnection (con)
'refresh the pivot table cache'
Worksheets(1).PivotTables(1).PivotCache.Refresh
'clear the cache of any orphaned items'
Dim pc As PivotCache
Dim ws As Worksheet
With ActiveWorkbook
For Each pc In .PivotCaches
pc.MissingItemsLimit = xlMissingItemsNone
Next pc
End With
End Sub
Change to the good connection every time the sheet opens
Private Sub Workbook_Open()
Dim con as vartype
Set con = ActiveWorkbook.Connections("MyGoodResultConnection")
Worksheets(1).PivotTables(1).ChangeConnection (con)
Worksheets(1).PivotTables(1).PivotCache.Refresh
End Sub
You should only need to setup the MyNoResultConnection connection on your local PC since users will not be calling PT_cache_clear().
Update:
Instead of always changing the pivot table connection every time you could set it conditionally by getting the current connection name from the PivotCache.WorkbookConnection property and comparing the names.
Worksheets(1).PivotTables(1).PivotCache.WorkbookConnection
Note:
It is not possible to just flush or empty the pivot cache. You can call a refresh method or set it to refresh automatically when the file opens. You can clear the cache of orphaned items.
PivotTable.ChangeConnection and PivotCache.WorkbookConnection can only be used with external data sources.
Setting PivotTable.SaveData to false will not clear the pivot cache. This stops the data source from being saved it does not affect the PivotCache. The PivotCache lies between the data source and pivot table. When using external data sources this property will have no effect and for OLAP data sources it is always false.
See posts here and here for getting the current state of your PivotCache.
Alternative ways to implement this:
Create the connection object and pivot table with a VBA macro when the workbook opens. Delete the connection, pivot table before the workbook closes. The cache will then be automatically deleted when the workbook is saved. This approach can be difficult depending on how your external datasource is setup. It may require your to store username and passwords inside your VBA code.
Related
I'm running MS Access 2016 connecting via ODBC to SQLServer 2016. I have a simple form based on a linked table. When I display it in Form View, change some data, and click for next record, it updates the current record and moves to the next, but when I use a combo box to select the record to move to - it displays the record I've selected and if I change some data on it - it returns the error
'ODBC -update on linked table failed;
Microsoft ODBC SQL Server Driver Query timeout expired (#0)’
I tried 2 different methods shown below by the code for the 2 different combo boxes
Private Sub cboFindRecord_AfterUpdate()
Dim rst As DAO.Recordset
Set rst = Me.RecordsetClone
rst.FindFirst "Id=" & Nz(Me.cboFindRecord, 0)
If Not rst.EOF Then
Me.Bookmark = rst.Bookmark
End If
rst.Close
Set rst = Nothing
End Sub
Private Sub FindRecord2_AfterUpdate()
Me.txtId.Enabled = True
Me.txtId.Locked = False
Me.txtId.SetFocus
If IsNull(Me.FindRecord2) Then
Exit Sub
End If
DoCmd.FindRecord Me.FindRecord2, acEntire, , acSearchAll, , acCurrent
End Sub
(I have this code on other forms in this database, and they work, but for this form, for this table it doesn’t)
There are 500 records on the table, its seems to be a blocking issue rather than a timeout issue.
If I run an Sp_who2 on SQL Server it shows there is a process block – but why, can anyone help me?
We've just had a similar issue with an Access system with a SQL backend, which was experiencing intermittent timeouts seemingly at random, that has also turned out to be a combo box issue:
We inherited this system and the original developers would use a combo box to lookup an IDs text equivalent and then reference that combo box from say a label. This meant that, for example, on the Sales Line form there was a hidden combo that contained the entire Stock table just to get the name of the stock item.
That combo appears to have been locking the entire Stock table and preventing inserts/updates/deletes.
We could consistently do the following to illustrate the issue:
Open a sales line on one copy of the system - that sales line has a combo that looks up data from the Stock table
Open the stock item in another copy of the system
Edit and save that stock item
At that point the system showing the stock item hangs
Then close the sales line on the first copy
That seems to release the lock on the Stock table as the stock item will then immediately save on the second copy
We resolved this by creating a SQL pass through query to the stock table and using that as the record source in the combo, rather than using an Access linked-table onto the Stock table in the SQL database as we had been previously. Doing that stopped the table locking occurring.
As you have noted that the form/table relationship work ok when using the record selector - - then the whole thing comes down to the combo box. The first thing I would do is simply create a new one from scratch. When you do that - be sure the key field is the first column and becomes the bound value of the combobox. This is the way it will want to set up inherently - but just be sure that is the case.
I am trying to create an excel addin which has set of Functions to pull value from the database (I use MS SQL Server). So my query will return only one record set. I use something like below in my vba code.
Using Excel VBA to run SQL query
But the problem with this is if I have my custom function in 100 cells, the macro makes connection to the DB everytime and retrive data from the DB.
Is there a way where I can make one connection and use that connection to write as many queries as I want?
Simple, run all your 100 function/ loops to access the database. One you've finished then close the connection. Look at your modified code below...
Option Explicit
Sub ConnectSqlServer()
Dim conn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim sConnString As String
' Create the connection string.
sConnString = "Provider=SQLOLEDB;Data Source=INSTANCE\SQLEXPRESS;" & _
"Initial Catalog=MyDatabaseName;" & _
"Integrated Security=SSPI;"
' Create the Connection and Recordset objects.
Set conn = New ADODB.Connection
Set rs = New ADODB.Recordset
' Open the connection and execute.
conn.Open sConnString
'>>>> run 100 loops
Dim i As Integer
For i = 1 To 100
Set rs = conn.Execute("SELECT * FROM Table" + 1 + ";")
' Check we have data.
If Not rs.EOF Then
' Transfer result.
' I assume that you've 100 Sheets
Sheets(i).Range("A1").CopyFromRecordset rs
' Close the recordset
rs.Close
Else
MsgBox "Error: No records returned.", vbCritical
End If
Next
' Clean up
If CBool(conn.State And adStateOpen) Then conn.Close
Set conn = Nothing
Set rs = Nothing
End Sub
I've add 100 loops and run it before the connection to database is closed.
Hope it useful.
In this kind of cases do not do db operations in loops. This is time consuming and not proper useage. Instead, in a loop create select, insert or whatsoever statement and then complate the loop operations and calculations then open the connection just one time and send request (created sql script) to db and get response from db then close connection. Thats all. db operations must be sparated from dailiy and recursive opeations.(antipattern) best regards.
Disclaimer: while this is not a direct solution to the problem described in the post I would like to add this approach as a much faster and easier solution to the problem described.
Step 1: create a (possibly hidden) sheet where you pull all the SQL data that you need in this Excel file. Pull the data into one table with all the necessary columns / dimensions to get the data afterwards from this table.
Here is what I mean by that. Let's assume that you need in this Excel file some data from the table Users on the SQL server as well as some information from a StockMarket table on the server. From the table Users you want the UserID, the first name, last name, and the job title. From the table StockMarket you will need the Stockmarket ID and the price for this particular share. As these prices are by date you also need the the quote date for the price.
Now, since you want all in one table you must think of a smart way to combine all this data into one table. One approach could be the following:
Afterwards you can get all the data from the above table with lookup functions like these:
=INDEX(SQLdata,MATCH(1,(SQLdata[Table]="Users")*(SQLdata[UserID]=25),0),4)
Note, that I named the table SQLdata to make it easier when looking at the formula and understanding it. Also, like this you can easily scan your Excel file for any reference to this table.
Another approach could be the following to make the table more concise:
Note, that this time I am mixing Strings with Numbers and also Strings with Dates (which is very poor design and for some people here even impossible to think of). Also, the column headers are now less descriptive. Yet, this works too:
=INDEX(SQLrev,MATCH(1,(SQLrev[Table]="Users")*(SQLrev[Dimension1]=25),0),5)
Note, that I called the table this time SQLrev.
Both solutions allow you also to aggregate data from the table. So, if you want (for example) the average price for Apple in 2017 then you can use the following formula to sum up the the quotes for this year and divide them by 3:
=SUM(IF("StockMarket"=SQLrev[Table];1;0)*IF("AAPL"=SQLrev[Dimension1];1;0)*SQLdata[Price])/3
The most notable advantage for this approach is that you merely have to update one table in the entire Excel file which means that there is only one SQL pull from the server.
The most notable disadvantage (apart from the SQL select which might get pretty complicated to write) is that you need to know of all the data that needs to reside in this table. If the data is not pulled into this table then none of the above formulas will be able to retrieve these values.
While this approach certainly has its downsides this is much easier to implement than the Excel AddIn you are aiming for.
All above formulas are array formulas and must be entered pressing Ctrl + Shift + Enter. For more information on array formulas please read the following: https://support.office.com/en-us/article/Guidelines-and-examples-of-array-formulas-7D94A64E-3FF3-4686-9372-ECFD5CAA57C7
So I have a particular situation here with the CRM I'm tasked with developing, and I'd like to see if I can get some insight on more of a sanctioned, best-practice approach for this specific scenario. Basically, I have a form called Accounts that displays information specific to that account as well as a subform that needs to display quotes and orders associated with that account to give the user a snapshot of work done.
Due to the complexity of the query and number of possible records, scrolling through this subform can be slow if using a SQL VIEW or more obviously, a local access query. The best solution here is a pass through query so I can shoot that processing straight to SQL.
I've done quite a bit of research and the best approach I've seen is to either 1) use VBA to change the definition of the pass through query or 2) use an INSERT INTO statement to populate a local table which can be used as the recordsource for that subform. At this point I just want to confirm that either or both of these approaches would qualify as a best practice situation and possibly an example of code to make this happen. Right now I'm struggling with where this processing belongs. I don't want to call the code from the subform that launches the main Accounts form since this form can be opened from a few different places and I don't want to duplicate that code.
Here is the code I'm using to copy the dataset from the pass through query to a local table and then using the Master/Child relationship fields to only show the appropriate data for that account.
Dim db As DAO.Database
Dim strSQL As String
Set db = CurrentDb
'Purge local table where pass through results are copied
strSQL = "Delete * FROM tblPassThruResults"
db.Execute strSQL
'Insert the results of the pass through query into local table
strSQL = "INSERT INTO tblPassThruResults Select Q_ACCOUNT_BUSINESS_OVERVIEW_PT.* FROM Q_ACCOUNT_BUSINESS_OVERVIEW_PT"
db.Execute strSQL
db.Close
Set db = Nothing
In this thread a user had problems with the lookup filter being missing in an acess database that had a SQL server backend. The problem was easily solved simply by checking on an option in the current database settings that allowed ODBC fields to also provide lookup filter dropdowns.
For those confused, the lookup filter is the excel like function in a datasheet view that allows you to click on the drop down of the field name and select individual values from that field for filters by a checkbox.
I, however, have a slightly different problem. The checkbox to allow ODBC field filter lookups is active in the settings, so that's not an issue. If I have a form that pulls data from a query, the lookup filters work fine, and are pre-populated with values in that field for filter selection. If that record source is changed in VBA, however, say for example, a SQL statement that exactly matches that query, the lookup filter no longer works. I tried creating a recordset and attaching it to the same form, creating a SQL statement and attaching it to the record source, and opening the form with arguments which are then used within the form's on load event to change the record source, all with the same result of no lookup filter.
Am I overlooking something?
After HansUp replied, I floundered about trying to figure out what he meant and ran into it rather accidently.
Since the form is opened by another form where the SQL statement is pregenerated, I simply assigned the form's underlying query to a QueryDef object and applied the pregenerated SQL statement to the object's SQL property. When the form was subsequently opened, the new SQL statement was used and all the lookup filters worked properly. I was quite pleased :D
Here's a quick run down of the code:
Dim db as Database
Dim qDef as QueryDef
Dim strSQL as String
'generate SQL statement and assign it to strSQL here'
Set qDef = db.QueryDefs("qryMyQuery")
qDef.SQL = strSQL
DoCmd.OpenForm "frmMyForm" 'frmMyForm is based of qryMyQuery'
Thanks, HansUp!
At work we've got a SQL Server database that several users connect to (read only) using Access 2003.
This was fine, except now one of the tables they look at is re-created fairly often and part of this process involves a cross-tab query.
Which means that, depending on the data, the number and and names of the columns potentially change each time the table is regenerated.
However when they look at the re-created table from Access, it still shows the column headings that were there when the table was first linked to.
Is there a way I can programmatically re-link the table each time they open the Access database?
What I ended up doing was creating a VBA function that looks something like below (needs error handling!)
Public Function ReConnectToLinkTable()
Dim db As Dao.Database
Dim tdf As Dao.TableDef
Set db = CurrentDb
Set tdf = db.CreateTableDef("local_table_name")
tdf.Connect = "ODBC;DRIVER=SQL Server;SERVER=server_name;UID=user_name;" & _
"PWD=password;APP=Microsoft Data Access Conponents;" & _
"DATABASE=database_name"
tdf.Attributes = TableDefAttributeEnum.dbAttachSavePWD
tdf.SourceTableName = "server_table_name"
db.TableDefs.Delete ("local_table_name")
db.TableDefs.Append tdf
End Function
Then I created a macro called AutoExec (the name guarantees it is called when the Access file is opened) which has an Action of RunCode, which calls the ReconnectToLinkTable() function.
ODBC linked tables break when the table or view on the server is altered. Some changes can result in them just becoming read-only, others will simply not include all the columns.
I have found that updating the connect string does not successfully fix this problem. It will usually fix missing fields, but it can still be read-only. The only reliable way to do this is to recreate the linked table on the fly.
Another alternative would be to not use a linked table at all, but use a saved QueryDef that has the appropriate connect string. This will never have to be updated, but could be a performance issue as the metadata stored in the table link helps Access figure out how to retrieve the data. Without that metadata stored in the table link, it has to retrieve that information from the server each time the query is run.
Something like this snippet is usually used. Search google for 'ms access refresh link table' and you'll find various solutions all similar to this one.