24

I have data in sheet 1. Normally I go to power query and do my transformations, then close, and load to an existing sheet 2.

I would like to automate this using VBA, where I can just run my power query automatically and populate the transformation to sheet 2.

Macro recorder doesn't seem to allow me to record the steps. And there isn't much online about doing this.

Trying some simpler code:

Sub LoadToWorksheetOnly()

'Sub LoadToWorksheetOnly(query As WorkbookQuery, currentSheet As Worksheet)
    ' The usual VBA code to create ListObject with a Query Table
    ' The interface is not new, but looks how simple is the conneciton string of Power Query:
    ' "OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & query.Name
     
    query = Sheets("Sheet6").Range("A1").value 'here is where my query from power query is. I put the text from power query avanced editor in another sheet cell.
    currentSheet = ActiveSheet.Name
    With ActiveSheet.ListObjects.Add(SourceType:=0, Source:= _
        "OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & query.Name _
        , Destination:=Sheets("target").Range("$A$1")).QueryTable
        .CommandType = xlCmdDefault
        .CommandText = Array("SELECT * FROM [" & query.Name & "]")
        .RowNumbers = False
        .FillAdjacentFormulas = False
        .PreserveFormatting = True
        .RefreshOnFileOpen = False
        .BackgroundQuery = True
        .RefreshStyle = xlInsertDeleteCells
        .SavePassword = False
        .SaveData = True
        .AdjustColumnWidth = True
        .RefreshPeriod = 0
        .PreserveColumnInfo = False
        .Refresh BackgroundQuery:=False
    End With
     
End Sub

Here is my issue when trying to load to new sheet manually.

enter image description here

Community
  • 1
  • 1
JonnyBoy
  • 385
  • 1
  • 2
  • 10
  • I set up the query, load to sheet 2. I dont know how to repeat this once I do it once. 2nd time it will not let me load to sheet 2. I thought it would be handy to have a code to do this for me. – JonnyBoy Jul 17 '18 at 17:20
  • Yes. I've updated my code , I want to do a query on sheet 1 and show the results in sheet 2 using vba, please see new code. – JonnyBoy Jul 17 '18 at 18:25
  • @Qharr, 1) This is correct logic I want to do, refresh then load to sheet 2, however I am having trouble loading it to sheet 2. I am right clicking on the query from show pane, then load to, then table. But where the data should be loaded is not letting me click (see image above). 2) Is there a vba code to automate refresh and load to sheet 2? – JonnyBoy Jul 18 '18 at 13:48
  • @QHarr thanks. Do you know if there is an automatic way to update this? doesnt have to be vba? – JonnyBoy Jul 18 '18 at 15:10
  • @QHarr, I am doing an internal query from excel sheets, joining two sheets to one. (sheet 1) – JonnyBoy Jul 19 '18 at 13:13
  • @QHarr , is there a vba way to refresh this query? – JonnyBoy Jul 19 '18 at 19:04
  • @QHarr , so your saying to have it preloaded into the sheet? And just use the code to refresh it? btw the refrsh code didnt work for me. where do i list the query name? – JonnyBoy Aug 01 '18 at 19:12
  • @QHarr thanks i understand . Having trouble refreshing table with vba code now. – JonnyBoy Aug 01 '18 at 19:17
  • @QHarr I believe I got it working :) thank you. But does the input and output both have to be in table format? I dont want the output to be in table format – JonnyBoy Aug 01 '18 at 19:29
  • No though that is an easy method. What is your data source and how is your data shaped? – QHarr Aug 01 '18 at 19:39
  • data source? Sheet 1 via table, Data shaped in a table. Not sure what you mean , tabular format, 5 columns in sheet 2. – JonnyBoy Aug 01 '18 at 20:50
  • I do some calculations on sheet 2. And seems like the table is making it tough for me to do this. Can you help me with doing it automatically? Does PQ have that ability? Or can PQ do a query on sheet 1 if it is not a table? That way no tables are involved. – JonnyBoy Aug 02 '18 at 13:44
  • It is difficult to advise without knowing what is going on. Are you saying that your processing of the source data involves calculations, or that after transformation you perform calculations? – QHarr Aug 02 '18 at 13:47
  • After I do the power query to sheet 2, I do some calculations with the table. The table makes it difficult to do these. So I would like to exclude tables completely if neccessary or at the very least the target spreadsheet. – JonnyBoy Aug 02 '18 at 13:58
  • Where are we at with this? I updated re calculations. – QHarr Aug 03 '18 at 12:49
  • I tried some of your suggestions , 1) When I go to query and refresh I get download failed, 2) I am getting an object defined error error on `ThisWorkbook.Worksheets("sheetname ").ListObjects("pivot").QueryTable.Refresh BackgroundQuery:=False` – JonnyBoy Aug 07 '18 at 14:18
  • It is going to be very difficult for me to debug at a distance I'm afraid. Why do you think it is giving a download failed message? Where are you loading data from that this message would occur? For the object defined: did you change to the appropriate sheet name, and table name? It is likely the two errors are somehow linked as you can't refresh what isn't there. But if a standard click of the refresh buttin is causing issues you need to open your advanced query editor, navigate to the top step and execute your query. And problem errors should be highlighted. – QHarr Aug 07 '18 at 14:21
  • Do I need to have a connection setup? All I am doing is adding data to sheet 1 and trying to refresh sheet 2 to see the newly added data. – JonnyBoy Aug 07 '18 at 14:33
  • Yes. The query should be set up to take data from sheet 1, process it and drop it in sheet 2. Once you have the query set up it will reproduce those steps. You add new data to sheet 1, press refresh, the data is sucked up, so to speak, processed and spat back out to sheet2. It will repeat the exact same steps you created when first setting up the query (except in case or errors where will halt with a warning). – QHarr Aug 07 '18 at 14:38

3 Answers3

34

VBA is absolutely suitable for automating PowerQuery and is particularly efficient for repetitive work. The trick is to create first the query you need in PowerQuery, then use the Advanced Editor to capture the M. Copy it and store it, either in a cell in the workbook, or in a separate text file.

The method is described in detail by Gil Raviv. For convenience, I store my M in text files instead of the workbook and load it with:

Function LoadTextFile(FullFileName As String) As String
  With CreateObject("Scripting.FileSystemObject")
    LoadTextFile = .OpenTextFile(FullFileName, 1).readall
  End With 
End Function

The nice thing about text files is that they are independent of excel and can re-used by many workbooks.

Here is some M:

let
// load the reference file (variables are shown in capitals;  
// variable values are replaced with strings from the excel control workbook)
    Source = Excel.Workbook(File.Contents(PATH_AND_NAME), null, true),
    ImportSheet = Source{[Item=SHEET_NAME,Kind="Sheet"]}[Data],
    #"Promoted Headers" = Table.PromoteHeaders(ImportSheet),
    #"Changed Type" = Table.TransformColumnTypes(#"Promoted Headers",{{"ACCOUNT", type text}})
in
    #"Changed Type"

Once loaded into VBA (from either workbook or text file), the M can be edited within VBA, for example by substituting placeholder words, or by using the M command names to locate and alter lines if necessary, e.g.

    ' create the M script to read the M file that will do the import
        M_Script = LoadTextFile(M_Source)
        
    ' insert the path
        M_Script = Replace(M_Script, "PATH_AND_NAME", """" & qSource & """") 
    
    ' insert the worksheet name
        If wksName <> "" Then M_Script = Replace(M_Script, "SHEET_NAME", """" & wksName & """")
        

The next step is to load the query. I do this using the technique described by Gil as follows:

Dim qry As WorkbookQuery

If DoesQueryExist(qName) Then 
    ' Deleting the query 
    Set qry = ThisWorkbook.Queries(qName) 
    qry.Delete 
End If 
           
Set qry = w.queries.Add(qName, M_Script, qSource)
                      
' We check if data should be loaded to Data Model 
shouldLoadToDataModel = ThisWorkbook.Worksheets(1).Cells(13, "D") 
 
' We check if data should be loaded to worksheet 
shouldLoadToWorksheet = ThisWorkbook.Worksheets(1).Cells(13, "E") 
 
If shouldLoadToWorksheet Then 
    ' We add a new worksheet with the same name as the Power Query query 
    Set currentSheet = Sheets.Add(After:=ActiveSheet) 
    currentSheet.Name = qName 
 
    If Not shouldLoadToDataModel Then 
        ' Let's load to worksheet only 
        LoadToWorksheetOnly qry, currentSheet 
    Else 
        ' Let's load to worksheet and Data Model 
        LoadToWorksheetAndModel qry, currentSheet 
    End If 
ElseIf shouldLoadToDataModel Then 
    ' No need to load to worksheet, only Data Model 
    LoadToDataModel qry 
End If 

Here is the LoadToDataModel function:

Option Explicit
Function LoadToDataModel(w As Workbook, query As WorkbookQuery, error As Integer) As Boolean
On Error GoTo Load_Error
    ' This code loads the query to the Data Model
    w.Connections.Add2 "Query - " & query.Name, _
        "Connection to the '" & query.Name & "' query in the workbook.", _
        "OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & query.Name _
        , """" & query.Name & """", 6, True, False

    LoadToDataModel = True
    
Load_Exit:
    Exit Function
    
Load_Error:
    LoadToDataModel = False
    error = Err.Number
    Resume Load_Exit
End Function

And the function for LoadToWorksheetOnly (thanks to wayback machine):

Sub LoadToWorksheetOnly(query As WorkbookQuery, currentSheet As Worksheet) 
    ' The usual VBA code to create ListObject with a Query Table 
    ' The interface is not new, but looks how simple is the conneciton string of Power Query: 
    ' "OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & query.Name 
     
    With currentSheet.ListObjects.Add(SourceType:=0, Source:= _ 
        "OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & query.Name _ 
        , Destination:=Range("$A$1")).QueryTable 
        .CommandType = xlCmdDefault 
        .CommandText = Array("SELECT * FROM [" & query.Name & "]") 
        .RowNumbers = False 
        .FillAdjacentFormulas = False 
        .PreserveFormatting = True 
        .RefreshOnFileOpen = False 
        .BackgroundQuery = True 
        .RefreshStyle = xlInsertDeleteCells 
        .SavePassword = False 
        .SaveData = True 
        .AdjustColumnWidth = True 
        .RefreshPeriod = 0 
        .PreserveColumnInfo = False 
        .Refresh BackgroundQuery:=False 
    End With 
     
End Sub 

Gil's code allows for importing data to either the data model or a worksheet. The OP requires the second, and if the method is followed, the transformed data should appear in the worksheet.

mer_curius
  • 524
  • 6
  • 12
  • 2
    The philosophy advocated in this answer is most relevant when the work is to be distributed to a wide audience with varying skill levels in Excel data manipulation. Setting up an automated data access method can leverage the benefits of remote data access and transforms without requiring your entire workforce to master the minutia of the extensive UI data menus. Many people struggle when encountering data in the abstract. – timlash Aug 28 '20 at 13:51
  • 1
    "The method is described in detail by Gil Raviv" but the link no longer works. Any chance you could add the missing subs (LoadToWorksheetOnly, LoadToDataModel) to your code above? – TheRizza Dec 21 '21 at 18:57
  • 2
    Added the missing subs from https://web.archive.org/web/20200318054225/https://gallery.technet.microsoft.com/office/VBA-to-automate-Power-956a52d1 – mer_curius Dec 23 '21 at 13:08
  • 1
    [Related question](https://stackoverflow.com/a/71590476/9245853) ... includes the code for `DoesQueryExist`. – BigBen Mar 23 '22 at 16:11
18

It would be easier to set your query up using the built-in tools, not VBA. It is NOT that you can't, as it seems at least one other answerer has misunderstood my point, it is just easier with the in-built, optimized, tools, and a lot faster to alter than continually copying your data elsewhere, escaping "", tracking back M language errors etc. You can then run that query via VBA.

You load your data via the appropriate method which can be from file, looping files in a folder, web, database.... the list goes on. You can import from external sources as well as load from internal. Have a look here for more information on loading from external sources.

Once you have secured your source and it is loaded you will be presented with the query editor where you can perform your transformation steps.

The point being that as you perform your steps using the UI, M code is written in the background and forms the basis of a re-usable query provided you don't change the source format or location. If you do, it is an easy edit to update the appropriate data source in the UI.

In your case, when you have performed your steps and have a query as you wish you then close and load to sheet2.

At this step, the first time you are setting this up you will select sheet 2 as your close and load destination:

Close and load

NB: When you select existing sheet, ensure Sheet 2 already exists and you can manually edit Sheet2! in front of the suggested range.


You are experiencing issues because you keep trying to recreate all of this with code.

Don't. Set it up using the UI and load to sheet2. From then on, either open the query editor to edit the steps and/or refresh the query to load the existing sheet2 with new/refreshed data.


Some of the available methods for refreshing your query:

The query will be refreshed by VBA/Manual refreshes to the sheet it resides in (Sheet2), or to the workbook itself e.g. Sheet2.Calculate , ThisWorkbook.RefreshAll, manually pressing the refresh workbook button in the data tab (these are all overkill really)

Refresh all tab

More targeted methods:

VBA for the query table in sheet 2:

ThisWorkbook.Worksheets("Sheet2").ListObjects(1).QueryTable.Refresh BackgroundQuery:=False   

Change the above to the appropriate table etc.

Right clicking in the querytable itself and selecting refresh:

Refresh

Click on the refresh button in the workbook queries window on the right hand side for the query in question (icon with green circling arrows)

Refresh


The Ken Pulls VBA way (minor edit from me)

Option Explicit
Public Sub UpdatePowerQueries()
    ' Macro to update my Power Query script(s)

    Dim lTest As Long, cn As WorkbookConnection
    On Error Resume Next
    For Each cn In ThisWorkbook.Connections
        lTest = InStr(1, cn.OLEDBConnection.Connection, "Provider=Microsoft.Mashup.OleDb.1", vbTextCompare)
        If Err.Number <> 0 Then
            Err.Clear
            Exit For
        End If
        If lTest > 0 Then cn.Refresh
    Next cn
    On Error GoTo 0
End Sub

There shouldn't be any real need for you to do all of this work via VBA. You may have some tricky data manipulation you feel more comfortable doing with VBA and then having PowerQuery access that processed data as source. You can fire off the whole lot by having a subroutine that calls the processing routine and then uses one of the VBA command methods listed above. There are more methods and I will add them when I have more time.


Calculations:

If you have calculations that depend on the PowerQuery output you have 4 obvious immediate options:

  1. Add these calculations where possible into PowerQuery. It supports calculated columns, user defined functions and lots more.
  2. Add the PowerQuery output to the data model and use the data model to perform calculations including calculated fields. This will give you access to time intelligence functions as well.
  3. Use VBA to add the calculations to the appropriate areas in sheet 2 if the range changes on refresh
  4. If range doesn't change on refresh simply put your formulas out of the way.
QHarr
  • 83,427
  • 12
  • 54
  • 101
3

A little late to this conversation, but I found it rather easy to update a Power Query in VBA by editing the Formula property.

First update the let..in formula. Then refresh the connections that use it

Dim pqFormula as String
pqFormula = "let..in"

Dim pqName as String
pqName = "<Name of the connection>"

' Update the formula for the specific power query.
ThisWorkbook.queries(pqName).Formula = pqFormula

' Refresh the connection
ThisWorkbook.Connections("Query - " & pqName).Refresh

All tables that rest on that connection should immediately update.

Regards,

MirelesJ
  • 53
  • 6