I have a working Excel spreadsheet which uses VBA to change a parameter in the connection query depending on what is entered into a single cell. It cannot use an Excel Parameter to pick up the value because the variable is in the join not in the where part of the query. So I know this works in principle, at least for one cell of data.
I now need to create a new spreadsheet where I need to put a range of data into the query.
The query looks something like this:
Select
*
FROM TABLE_A
Where ID in
('A',
'B',
'C')
The VBA picks up the values A, B and C from a column in the spreadsheet, and currently looks like this:
Dim ID_Range As Range
Sheets("Data").Select
Set ID_Range = Sheets("Data").Range("A1:A10")
With ActiveWorkbook.Connections("Query from Database_A").ODBCConnection
.BackgroundQuery = True
.CommandText = Array( _
"Select * FROM Table_A A WHERE A.ID in " "(" + ID_Range + ")")
.CommandType = xlCmdSql
.Connection = Array(Array( _
ODBC;Description= ****
.RefreshOnFileOpen = False
.SavePassword = False
.SourceConnectionFile = ""
.SourceDataFile = ""
.ServerCredentialsMethod = xlCredentialsMethodIntegrated
.AlwaysUseConnectionFile = False
End With
I hope I haven't removed any important code when removing the company specific information.
When this is run it comes up with the error: Subscript out of Range.
What do I need to do to get this to work?
If you don't have a specific need of the ODBCConnection object (and I don't see its advantage in your situation) you can do this quite simply using ADODB or even good old DAO!
' Create a recordset object.Dim rsPubs As ADODB.Recordset
Set rsPubs = New ADODB.Recordset
With rsPubs
' Assign the Connection object.
.ActiveConnection = cnPubs
' Extract the required records.
.Open "SELECT * FROM Authors"
' Copy the records into cell A1 on Sheet1.
Sheet1.Range("A1").CopyFromRecordset rsPubs
' Tidy up
.Close
End With
cnPubs.Close
Set rsPubs = Nothing
Set cnPubs = Nothing
(code sample pasted from https://support.microsoft.com/fr-fr/help/306125/how-to-import-data-from-microsoft-sql-server-into-microsoft-excel)
The best way would be to create a function to process the range
Function getCommaSeparatedList(Source As Range) As String
Dim cell As Range
Dim results As String
For Each cell In Source
results = results & "'" & cell.Value & "',"
Next
getCommaSeparatedList = Left(results, Len(results) - 1)
End Function
Try like this.
Dim ID_Range As Range
Dim vR() As String
Dim n As Integer
Dim strRange As String
Sheets("Data").Select
Set ID_Range = Sheets("Data").Range("A1:A10")
For Each Rng In ID_Range
n = n + 1
ReDim Preserve vR(1 To n)
vR(n) = "'" & Rng & "'"
Next Rng
strRange = Join(vR, ",")
With ActiveWorkbook.Connections("Query from Database_A").ODBCConnection
.BackgroundQuery = True
.CommandText = Array("Select * FROM Table_A A WHERE A.ID in (" & strRange & ")")
Related
Im using Access 2013 and Excel 2013. In terms of References, I am using Microsoft Office 15.0 Access database engine Object Library.
So I am trying to run an INSERT INTO query from VBA. The worksheet has a list of part numbers, which I used this code to convert into an array.
Function partArray()
Dim partList() As Variant
Dim partArr(10000) As Variant
Dim x As Long
partList = ActiveWorkbook.Worksheets("Parts").ListObjects("Parts").ListColumns("Part Number").DataBodyRange.Value
For x = LBound(partList) To UBound(partList)
partArr(x) = partList(x, 1)
Next x
partArray = partArr
End Function
Now I am trying to use an INSERT INTO query to input these part numbers into a table in access. Any idea how I can do this?
You should use ADO to connect between Excel and Access. It will be a reference under Tools/References in the VBE. Using ADO you can run SQL statements. You can define your table in Excel as the origin table and then read data from that, put them into a recordset and then write the recordset into an Access table. There are plenty of examples on the internet. You can start with this: https://www.exceltip.com/import-and-export-in-vba/export-data-from-excel-to-access-ado-using-vba-in-microsoft-excel.html
Whoa! I think your approach is totally wrong. Try something like this.
Sub ADOFromExcelToAccess()
' exports data from the active worksheet to a table in an Access database
' this procedure must be edited before use
Dim cn As ADODB.Connection, rs As ADODB.Recordset, r As Long
' connect to the Access database
Set cn = New ADODB.Connection
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0; " & _
"Data Source=C:\FolderName\DataBaseName.mdb;"
' open a recordset
Set rs = New ADODB.Recordset
rs.Open "TableName", cn, adOpenKeyset, adLockOptimistic, adCmdTable
' all records in a table
r = 3 ' the start row in the worksheet
Do While Len(Range("A" & r).Formula) > 0
' repeat until first empty cell in column A
With rs
.AddNew ' create a new record
' add values to each field in the record
.Fields("FieldName1") = Range("A" & r).Value
.Fields("FieldName2") = Range("B" & r).Value
.Fields("FieldNameN") = Range("C" & r).Value
' add more fields if necessary...
.Update ' stores the new record
End With
r = r + 1 ' next row
Loop
rs.Close
Set rs = Nothing
cn.Close
Set cn = Nothing
End Sub
Or, this.
Sub DAOFromExcelToAccess()
' exports data from the active worksheet to a table in an Access database
' this procedure must be edited before use
Dim db As Database, rs As Recordset, r As Long
Set db = OpenDatabase("C:\FolderName\DataBaseName.mdb")
' open the database
Set rs = db.OpenRecordset("TableName", dbOpenTable)
' get all records in a table
r = 3 ' the start row in the worksheet
Do While Len(Range("A" & r).Formula) > 0
' repeat until first empty cell in column A
With rs
.AddNew ' create a new record
' add values to each field in the record
.Fields("FieldName1") = Range("A" & r).Value
.Fields("FieldName2") = Range("B" & r).Value
.Fields("FieldNameN") = Range("C" & r).Value
' add more fields if necessary...
.Update ' stores the new record
End With
r = r + 1 ' next row
Loop
rs.Close
Set rs = Nothing
db.Close
Set db = Nothing
End Sub
Of course you could use the TransferSpreadsheet method if you want.
Option Explicit
Sub AccImport()
Dim acc As New Access.Application
acc.OpenCurrentDatabase "C:\Users\Public\Database1.accdb"
acc.DoCmd.TransferSpreadsheet _
TransferType:=acImport, _
SpreadSheetType:=acSpreadsheetTypeExcel12Xml, _
TableName:="tblExcelImport", _
Filename:=Application.ActiveWorkbook.FullName, _
HasFieldNames:=True, _
Range:="Folio_Data_original$A1:B10"
acc.CloseCurrentDatabase
acc.Quit
Set acc = Nothing
End Sub
I currently have code to allow me to look through the rows with matching ID from Sheet 1 and Sheet 2. When both IDs match, Sheet 2 information will be pasted to the Sheet 1 rows with the same IDs. My code works on less than 1,000 rows and when I tested it gave results within a minute.
The problem is that when I tried to run it for 1,000,000 rows it keeps running and for more than 20 minutes and never stop running since then. I hope anyone could assist me in making changes to the code to allow me to do a loop and copy paste the information from Sheet 2 to Sheet 1 for 200,000 rows.
Sub Sample()
Dim tracker As Worksheet
Dim master As Worksheet
Dim cell As Range
Dim cellFound As Range
Dim OutPut As Long
Set tracker = Workbooks("test.xlsm").Sheets("Sheet1")
Set master = Workbooks("test.xlsm").Sheets("Sheet2")
Application.ScreenUpdating = False
For Each cell In master.Range("A2:A200000")
Set cellFound = tracker.Range("A5:A43000").Find(What:=cell.Value, LookIn:=xlValues, LookAt:=xlWhole)
If Not cellFound Is Nothing Then
matching value
cellFound.Offset(ColumnOffset:=1).Value2 = cell.Offset(ColumnOffset:=2).Value2
Else
End If
Set cellFound = Nothing
Debug.Print cell.Address
Next
Application.ScreenUpdating = True
OutPut = MsgBox("Update over!", vbOKOnly, "Update Status")
End Sub
Above is the code that I have for now.
Incorporating #paulbica's suggestion, this ran in several seconds for me.
Sub Sample()
Dim rngTracker As Range
Dim rngMaster As Range
Dim arrT, arrM
Dim dict As Object, r As Long, tmp
With Workbooks("test.xlsm")
Set rngTracker = .Sheets("Tracker").Range("A2:B43000")
Set rngMaster = .Sheets("Master").Range("A2:C200000")
End With
'get values in arrays
arrT = rngTracker.Value
arrM = rngMaster.Value
'load the dictionary
Set dict = CreateObject("scripting.dictionary")
For r = 1 To UBound(arrT, 1)
dict(arrT(r, 1)) = r
Next r
'map between the two arrays using the dictionary
For r = 1 To UBound(arrM, 1)
tmp = arrM(r, 1)
If dict.exists(tmp) Then
arrT(dict(tmp), 2) = arrM(r, 3)
End If
Next r
rngTracker.Value = arrT
End Sub
You could use the index of a Dictionary object and use its native indexing properties to perform the lokups. I'm not sure just how well that will perform in a data set of 200K records where a high report of failure was going to occur and you are showing at least a 78% failure rate (200K records to match and update 43K records).
Sub Sample3()
Dim tracker As Worksheet, master As Worksheet
Dim OutPut As Long
Dim v As Long, p As Long, vMASTER As Variant, vTRACKER As Variant, dMASTER As Object
Set tracker = Workbooks("test.xlsm").Sheets("Sheet1")
Set master = Workbooks("test.xlsm").Sheets("Sheet2")
Set dMASTER = CreateObject("Scripting.Dictionary")
Debug.Print Timer
'Application.ScreenUpdating = False '<~~no real need to do this if working in memory
With tracker
vTRACKER = .Range(.Cells(5, 2), .Cells(Rows.Count, 1).End(xlUp)).Value2
End With
With master
vMASTER = .Range(.Cells(2, 1), .Cells(Rows.Count, 3).End(xlUp)).Value2
For v = LBound(vMASTER, 1) To UBound(vMASTER, 1)
If Not dMASTER.exists(vMASTER(v, 1)) Then _
dMASTER.Add Key:=vMASTER(v, 1), Item:=vMASTER(v, 3)
Next v
End With
For v = LBound(vTRACKER, 1) To UBound(vTRACKER, 1)
If dMASTER.exists(vTRACKER(v, 1)) Then _
vTRACKER(v, 2) = dMASTER.Item(vTRACKER(v, 1))
Next v
With ThisWorkbook.Sheets("Sheet1") 'tracker
.Cells(5, 1).Resize(UBound(vTRACKER, 1), 2) = vTRACKER
End With
'Application.ScreenUpdating = True '<~~no real need to do this if working in memory
Debug.Print Timer
OutPut = MsgBox("Update over!", vbOKOnly, "Update Status")
dMASTER.RemoveAll: Set dMASTER = Nothing
Set tracker = Nothing
Set master = Nothing
End Sub
Once both ranges are mirrored into variant arrays, a dictionary is created in order to fully utilize its indexing properties for identification.
The above shows about a significant increase in efficiency over 200K records in master vs 43K records in tracker.
btw, I did use an .XLSB for this; not an .XLSM.
It might also be faster to use ADODB.
Dim filepath As String
Dim conn As New ADODB.Connection
Dim sql As String
filepath = "c:\path\to\excel\file\book.xlsx"
With conn
.Provider = "Microsoft.ACE.OLEDB.12.0"
.ConnectionString = "Data Source=""" & filepath & """;" & _
"Extended Properties=""Excel 12.0;HDR=No"""
sql = _
"UPDATE [Sheet1$A2:B200000] AS master " & _
"INNER JOIN [Sheet2$] AS tracker ON master.F1 = tracker.F1 " & _
"SET master.F2 = tracker.F2"
.Execute sql
End With
This works with Office 2007. Office 2010 (I haven't tested on 2013) has a security measure that prevents updating Excel spreadsheets with an SQL statement. In this case you can either use the old Jet provider, which doesn't have this security measure. This provider doesn't support .xlsx, .xlsm or .xlsb files; only .xls.
With conn
.Provider = "Microsoft.Jet.OLEDB.4.0"
.ConnectionString = "Data Source=""" & filepath & """;" & _
"Extended Properties=""Excel 8.0;HDR=No"""
Alternatively, you can read the resulting data into a disconnected recordset and paste the recordset into the original worksheet:
Dim filepath As String
Dim conn As New ADODB.Connection
Dim sql As String
Dim rs As New ADODB.Recordset
filepath = "c:\path\to\excel\file\book.xlsx"
With conn
.Provider = "Microsoft.ACE.OLEDB.12.0"
.ConnectionString = "Data Source=""" & filepath & """;" & _
"Extended Properties=""Excel 12.0;HDR=No"""
sql = _
"SELECT master.F1, IIF(tracker.F1 Is Not Null, tracker.F2, master.F2) " & _
"FROM [Sheet1$A2:B200000] AS master " & _
"LEFT JOIN [Sheet2$] AS tracker ON master.F1 = tracker.F1 "
rs.CursorLocation = adUseClient
rs.Open sql, conn, adOpenForwardOnly, adLockReadOnly
conn.Close
End With
Workbooks.Open(filepath).Sheets("Sheet1").Cells(2, 1).CopyFromRecordset rs
If using CopyFromRecordset, keep in mind that there is no guarantee of the order in which the records are returned, which might be a problem if there is other data in the master worksheet besides columns A and B. To resolve this, you can include those other columns in the recordset as well. Alternatively, you can enforce the order of the records using an ORDER BY clause, and sort the data in the worksheet before you begin.
I am having an issue (otherwise I wouldn't be here) :)
In Excel I have a list of item numbers that need to be checked if they already exist in a database. At the moment the only way to do that is to run a query in a separate sheet to get all the item numbers (400,000+) which takes quite some time and has to be done each time.
I have started a vba query which goes through each cell that is selected and checks that value to see if it exist in the database. If it exist, the cell turns Red. If it doesn't exist, the cell turns green.
I'm not that great when it comes to querying databases using VBA so I used bits and pieces that I found online.
When I go to test it, Excel crashes and closes so I can't determine where its going wrong.
Public Sub CheckItemNoExist()
Dim DB As New ADODB.Connection
Dim rs As New ADODB.Recordset
Dim c As Range
Dim ItemNum As Variant
Dim bFound As Boolean
DB.Open "DSN=DBNAME;UID=****;PWD=****;"
Set rs = DB.OpenRecordset("SELECT [Inventory.ITM_NO] FROM [OAUSER.Inventory] WHERE [Inventory.ITM_NO]=" & ItemNum & ";", dbOpenDynaset)
For Each c In ActiveSheet.Selection
ItemNum = c.Value
bFound = Not rs.EOF
If bFound Then
c.Interior.Color = RGB(255, 0, 0)
Else
c.Interior.Color = RGB(0, 255, 0)
End If
Next
rs.Close
Set rs = Nothing
DB.Close
Set DB = Nothing
End Sub
Thank you!
Matt
I suggest you use an IN in your where clause to bring over all the items in the selection, then filter the recordset for each item to see if it found it. I don't have your data setup, so I used a truck database that I have handy. You should be able to convert
Sub CheckTruckExists()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim sSql As String
Dim aInClause() As String
Dim rCell As Range
Dim vaTrucks As Variant
Dim i As Long
'Open a connection to the database
Set cn = New ADODB.Connection
cn.Open sCONNECTIONSTRING
'Get an array of values from the selection
vaTrucks = Selection.Value
'Increase the size of the one-dimensional array to match
ReDim aInClause(LBound(vaTrucks, 1) To UBound(vaTrucks, 1))
'Fill a one-dim array from the two-dim array so we can Join it
For i = LBound(vaTrucks, 1) To UBound(vaTrucks, 1)
aInClause(i) = vaTrucks(i, 1)
Next i
'Build the SQL statement and execute it
sSql = "SELECT ReportTruck FROM qryTrucks WHERE ReportTruck IN ('" & Join(aInClause, "','") & "')"
Set rs = New Recordset
Set rs = cn.Execute(sSql)
'Loop through the selected cells
For Each rCell In Selection.Cells
'clear the filter, then refilter the recordset on the filtered value
rs.Filter = adFilterNone
rs.Filter = "ReportTruck = '" & rCell.Value & "'"
'If the filter returned zero records, it's eof
If rs.EOF Then
rCell.Interior.Color = RGB(255, 0, 0)
Else
rCell.Interior.Color = RGB(0, 255, 0)
End If
Next rCell
End Sub
My ReportTruck field is a String, so I have to enclose all the trucks in my IN clause with single quotes. If you're looking for something other than a String, modify the Join to accommodate that data type.
Also, there's no error checking to makes sure the Selection is actually a Range object or that it contains more than one cell - both are necessary to avoid an error.
I'm using Excel to pull data from an SQL db. I used the code from another SO question and it works fine. Now I want to pull in the column names from a table in addition to the actual table. I figured out that I could get the names using the For Each fld loop. However there's still the issue of populating them horizontally in a row in Excel as the number of columns might change - so I'm thinking I would need another For each loop also or something similar.
Sub GetDataFromADO()
'Declare variables'
Set objMyConn = New ADODB.Connection
Set objMyCmd = New ADODB.Command
Set objMyRecordset = New ADODB.Recordset
'Open Connection'
objMyConn.ConnectionString = "Provider=SQLOLEDB;Data Source=localhost;User ID=abc;Password=abc;"
objMyConn.Open
'Set and Excecute SQL Command'
Set objMyCmd.ActiveConnection = objMyConn
objMyCmd.CommandText = "select * from myTable"
objMyCmd.CommandType = adCmdText
objMyCmd.Execute
'Loop Names'
' WHAT TO DO HERE????'
'Open Recordset'
Set objMyRecordset.ActiveConnection = objMyConn
objMyRecordset.Open objMyCmd
'Copy Data to Excel'
ActiveSheet.Range("A1").CopyFromRecordset (objMyRecordset)
End Sub
My usual code is very similar:
For intColIndex = 0 To objMyRecordset.Fields.Count - 1
Range("A4").Offset(0, intColIndex).Value = objMyRecordset.Fields(intColIndex).Name
Next
Ok so I figured it out after 4 attempts, here's the code for the loop.
'Loop'
Dim FieldRange As Range
Set FieldRange = Range("A4")
Set TableColumns = Range("A4:H4")
x = 1
Range("A4").Select
For Each fld in objMyRecordset.Fields
ActiveCell.Value = fld.Name
ActiveCell.Offset(0, x).Select
x = x + 1 'tick iterator
Next
ActiveSheet.Range("A5").CopyFromRecordset objMyRecordset
Range("A4").Select
To make it super simple, do something like this (using Sheet1 and recordset r)
For i = 0 To r.Fields.Count - 1
Sheet1.Cells(1, i + 1) = r.Fields(i).Name
Next i
You can just set your "x" variable to 0 and then do something like:
x = 0
For Each Field In RS.Fields 'RS being my Recordset variable
Range("A3").Offset(0, x).Value = Field.Name
x = x + 1
Next Field
And that will make it a bit easier to read... :)
I have a range I'd like to arbitrarily sort and filter using vba. I don't, however, want it to affect the worksheet. I'd like to essentially copy the range into some native class that supports filtering and sorting (so i don't have to reinvent the wheel) and use that class to return a result to calling code.
Are there any classes I can use to this end? ListObject looked the most promising but it appears to require being tied to a worksheet range to work properly.
You can use recordsets. Here are some notes:
'Reference: Microsost ActiveX n.n Object Library '
Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
'From: http://support.microsoft.com/kb/246335 '
strFile = Workbooks(1).FullName
strCon = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & strFile _
& ";Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"";"
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
cn.Open strCon
'Must have client-side cursor for sorting '
rs.CursorLocation = adUseClient
'Substitute a name range for [Sheet1$] '
'or include a range of cells : [Sheet1&A1:C7] '
strSQL = "SELECT * FROM [Sheet1$] " _
& "WHERE TransID>2 ORDER BY MyTime"
rs.Open strSQL, cn, 3, 3
rs.Filter = "TransID=3"
rs.Sort = "Mytime"
'Write out to another sheet '
Worksheets(2).Cells(2, 1).CopyFromRecordset rs
You may find this thread interesting: syncing two lists with VBA
If you'd like to read and parse complex sets of data you can use the Microsoft ActiveX Data Objects Recordset 2.8 Library. With this you can read your data into a recordset, then filter, sort, append, delete and pass it to other functions.
I regularly use this, because i often have to manipulate and display large datasets. If it's in a recordset i can use the same manipulation and presentation routines over and over again.
See Merge Excel Sheets.... for an example of throwing data into a recordset. After you have the data in a recordset then use r.filter = "ColumnA = 1", or r.sort = "ColumnC, ColumnA".
Turns out I can create a recordSet to do this. Unlike, Remou's answer though we don't have to invoke a heavy weight odbc process on our sheet.
The following function (adapted from Mark Nold's answer) will create a record set from the supplied range. It assumes column headers are in the first row of the supplied range. This can be made more robust but its a good starting spot
Function CreateRecordSet(rSource As range) As Recordset
' Constants
Const MAX_CHARS = 1200
' Declarations
Dim rs As Recordset
Dim c As Long
Dim r As Long
Dim colCount As Long
Dim rowCount As Long
Dim fldName As String
colCount = rSource.Columns.Count
rowCount = rSource.rows.Count
' Setup record set
Set rs = New Recordset
r = 1 ' assume first row contains headers
For c = 1 To colCount
fldName = rSource.Cells(r, c).Value
rs.Fields.Append fldName, adVarChar, MAX_CHARS
Next c
' Fill record set
rs.Open
r = 2 ' skip header row
For r = 2 To rowCount
rs.AddNew
Debug.Print "row "; r & " of " & rowCount & " created"
For c = 1 To colCount
rs.Fields(c - 1) = CStr(rSource.Cells(r, c).Value)
Debug.Print "-- row(" & r; "): added col " & c & " of " & colCount
Next c
Next r
Set CreateRecordSet = rs
End Function
Sub TestCreateRecordSet()
Dim r As range
Dim rs As Recordset
Set r = range("A1:B4")
Set rs = CreateRecordSet(r)
End Sub
You want to use a Range class (just like CasperOne says). Here's some example VBA code
Function SortAndFilter(rSource As Range) As Range
Dim rResult As Range
Dim vaTemp As Variant
Dim wsTemp As Worksheet
Dim wbTemp As Workbook
vaTemp = rSource.Value
Set wbTemp = Workbooks.Add
Set wsTemp = wbTemp.Sheets(1)
Set rResult = wsTemp.Range("A1").Resize(UBound(vaTemp, 1), UBound(vaTemp, 2))
rResult.Value = vaTemp
rResult.Sort rResult.Cells(1), xlDescending
Set SortAndFilter = rResult
End Function
Sub Testit()
Dim rTest As Range
Set rTest = SortAndFilter(Selection)
'Do stuff with your range object
rTest.Parent.Parent.Close False 'close temp workbook
End Sub
Why not copy the data to a new, hidden worksheet, perform your sort/filter there, and then copy the data back when done?